shaplessでdeclineのOptsを導出する

shaplessのLabelledGenericを使ってdeclineのOptsを自動導出します。
2022.10.31

はじめに

declineではオプション引数ごとに詳細を指定してOptsを生成する必要があります。case classのフィールド名をそのままオプション名にするので十分な場合にはOptsを生成するコードは冗長になります。今回はshapelessを使ってcase classの定義からOptsを導出してみます。

やること

case classの各フィールドから対応するOptsを導出し、case class自体のOptsへ合成します。以下のコードのようなイメージです。

final case class Config(name: String, favoriteNumber:Int)
val name = Opts
val favoriteNumber = Opts
val configOpts = (name, favoriteNumber).mapN(Config.apply)

Optsの導出はshaplessを使った型クラスインスタンスの生成における典型的なパターンです。Optsを直接導出することはできないのでOptsを生成するOptGenを導出します。

import cats.implicits._
import com.monovore.decline.{Argument, Opts}
import shapeless.labelled.{FieldType, field}
import shapeless.{::, HList, HNil, LabelledGeneric, Lazy, Witness}

trait OptGen[A] {
  def opts: Opts[A]
}

object OptGen {

  def apply[A](implicit opts: Lazy[OptGen[A]]): OptGen[A] = opts.value

  private final case class Gen[A](opts: Opts[A]) extends OptGen[A]

  implicit def genericOpt[A, R <: HList](implicit
      generic: LabelledGeneric.Aux[A, R],
      opts: Lazy[OptGen[R]]
  ): OptGen[A] =
    Gen(opts.value.opts.map(generic.from))

  implicit def hlistOpt[K <: Symbol, H, T <: HList](implicit
      hOpts: Lazy[OptGen[FieldType[K, H]]],
      tOpts: OptGen[T]
  ): OptGen[FieldType[K, H] :: T] = Gen(
    (hOpts.value.opts, tOpts.opts).mapN(_ :: _)
  )

  implicit def fieldOpt[K <: Symbol, T](implicit
      witness: Witness.Aux[K],
      arg: Argument[T]
  ): OptGen[FieldType[K, T]] =
    //ヘルプ文字列は割り切り!!
    Gen(Opts.option[T](witness.value.name, witness.value.name).map(field[K](_)))

  implicit val hnilOpt: OptGen[HNil] = Gen(Opts.unit.as(HNil))
}

実行してみる

導出したOptsでのパースも明示的に生成した場合と同様に行えます。

final case class Config(name: String, favoriteNumber:Int)
val opts = OptGen[Config].opts
val command = Command("opts example", "")(opts)
val result = command.parse(Seq("--name","foobar", "--favoriteNumber", "128"))

println(result) //Right(Config(foobar,128))

まとめ

いつもながらshapelessは強力だと思います。