declineでタグ付き型のオプションをパースする

scalaのコマンドラインパーサーdeclineでタグ付きの型をパースしてみました。
2022.10.31

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

decline はパーサを組み合わせてコマンドライン引数およびオプションのパーサを構築できる便利なライブラリです。これでタグ付き型を扱いたかったので試してみました。

タグ付き型

今回扱うのは以下のようなよくみるタグ付き型です。

(refined typeについてはdecline-refinedで対応しているようです)

sealed trait UserNameTag
sealed trait PasswordTag
type UserName = String with UserNameTag
type Password = String with PasswordTag

これを以下のようにOptsとして定義します。

val usernameOpts = Opts
    .option[UserName]("username", "username for BASIC auth", "u")
val passwordOpts =
    Opts.option[Password]("password", "password for BASIC auth", "p")

しかしUserNameおよびPasswordは独自の型なのでArgumentが見つからない旨のコンパイルエラーとなります。

Argument

既存の型(String)にタグをつけただけの場合はdeclineに含まれているインスタンスから生成できます。以下のようにタグ付きに変換するインスタンスをでっちあげて宣言しておきます。

implicit defではインスタンスの解決が発散(diverging implicit expansion)してしまいました。

def tagged[A, T](implicit arg: Argument[A]): Argument[A with T] =
      arg.map(_.asInstanceOf[A with T])

implicit val usernameArg = tagged[String, UserNameTag]
implicit val passwordTag = tagged[String, PasswordTag]

実行してみる

コマンドを定義して実行してみます。

final case class Config(
    username: UserName,
    password: Password
)
val usernameOpts = Opts
  .option[UserName]("username", "username for BASIC auth", "u")
val passwordOpts =
  Opts.option[Password]("password", "password for BASIC auth", "p")

val configOpts = (usernameOpts, passwordOpts).mapN(Config.apply)

val command = Command("tagged option", "demo for tagged options")(configOpts)
println(command.parse(Seq("--username", "anne", "--password", "@ws0m3b1rd")))
//Right(Config(anne,@ws0m3b1rd))

タプルをスワップするとコンパイルエラーになります。

val configOpts = (usernameOpts, passwordOpts).swap.mapN(Config.apply) //エラー

まとめ

一手間増えますがパラメータが多い場合でもcase classへの変換時に型チェックができるのでミスが少なくなると思います。