そんなシチュエーションあるんか?と思うけど、ある。
今Chrome Devtools ProtocolをScalaで実装してて、もう動くところまで行ってるんだけど、スクリーンショットを撮影するAPIがこんな感じで定義されている。
このうちquality
引数はformat
引数が"jpeg"
のときにのみ使うことができる。別に"jpeg"
でない時に値を入れて送っても無視されるだけだが、これを型で守るにはどうしたら良いかな〜と妄想していた。
まず最低限必要な引数を入れた基礎となるシグネチャはこんな感じになるはず。
def captureScreenshot(format: "png" | "jpeg", quality: Option[Int]= None) = ???
format
は最低限pngとjpegがあるとしてそのままリテラル文字列型を書き、union typesでどっちか選ばせることができる。で、quality
はoptionalな引数なのでそのままOption[Int]
にし、デフォルト値をNone
で埋める*1。
Match types
さて、型レベルの制約「format
がjpegのときにだけquality
をいじって良い」を導入したい。ある型が"jpeg"
になっているときに引数を有効化するということは、次のように言い換えることができる。
- ある型Fが"jpeg"のときは
Option[A]
である - ある型Fが"jpeg"でないときはつねに
None
である
Scala 3では、match typesを利用して「型パラメータが特定の型の場合はこの型になる」といった型を定義できる。まさに今回のユースケースに合致する。
今回の要件をmatch typesで表現すると以下のようになる:
type OnlyJpeg[F, A] = F match case "jpeg" => Option[A] case _ => None.type // Noneはシングルトンなので.typeを補足する
試しにコードを書くとちゃんとこの制約を満たすことがわかる:
val x: OnlyJpeg["png", Int] = Some(42) // Required: None.type val y: OnlyJpeg["jpeg", Int] = Some(42) // コンパイル通る
型パラメータを利用する
さて、match typesを使って型レベルの制約を導入できた。これを当初予定していたシグネチャに当てはめてみる:
def captureScreenshot(format: "png" | "jpeg", quality: OnlyJpeg[???, Int] = None) = ???
OnlyJpeg
の1つめの型パラメータに何を入れたらよいかわからなくなってしまった。format
引数に入れた型を取り出すためには、いったん型パラメータに持ち上げて変数のように扱えるようにする必要がある:
def captureScreenshot[F <: "png" | "jpeg"](format: F, quality: OnlyJpeg[F, Int] = None) = ???
こうすることで、実際に選ばれるフォーマットの型はまずF
に代入され、次にOnlyJpeg
の型が通るかが確認されるようになる。
この「いったん型パラメータに持ち上げて変数化する」という技法は型レベルでいろいろ弄るときには頻出するので覚えておくとよい。
普通にオーバーロードする
まぁ今回の場合はメソッドをオーバーロードして多重定義したほうがシンプルだ。
def captureScreenshot(format: "png") = ??? def captureScreenshot(format: "jpeg", quality: Option[Int] = None) = ???
Match typesを使うテクはどうしても単一のメソッドでなんとかしたい場合や、有効/無効の組み合わせが沢山ある場合にどうぞ。覚えておくととりあえず便利です。
*1:Scalaはオプショナル引数まわりの構文糖をもう少し充実させてほしい