Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Scala3で引数の型によって他の引数を有効化したり無効化したりする

そんなシチュエーションあるんか?と思うけど、ある。

今Chrome Devtools ProtocolをScalaで実装してて、もう動くところまで行ってるんだけど、スクリーンショットを撮影するAPIがこんな感じで定義されている。

chromedevtools.github.io

このうち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はオプショナル引数まわりの構文糖をもう少し充実させてほしい

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?