Scala 3にはsummon
という関数がある。普段はあまり使うことがないが、知っておくとusing
まわりのデバッグにも使えるとても便利なやつだ。この記事ではsummon
の便利な利用法を紹介する。
型の一致を確認する
summon
の典型的なユースケースは、2つの型が一致するかを確認することだ。
type I = Int summon[I =:= Int] // コンパイルが通る
summon[A =:= B]
と書くと、型A
とB
が等しい場合にはコンパイルが通り、そうではない場合にはコンパイルに失敗する。サブタイプかどうかを見てほしいときは<:<
を使える。
summon[String =:= Int]
-- [E172] Type Error: ---------------------------------------------------------- 1 |summon[String =:= Int] | ^ | Cannot prove that String =:= Int. 1 error found
どういうときに2つの型が一致するか確認したいのかというと、例えば型レベル計算がうまくいっているかを知りたいときに便利だ。要するにこの型ってこの型に展開されるんだよね?というのがわかるのだ。
例えば、型パラメータに渡したIntのぶん次元を持つベクトルVec
型を次のように実装したとする(中身は難しいから今は知らなくていい)。
import scala.compiletime.ops.int._ type Vec[N <: Int] = N match { case 0 => EmptyTuple case _ => Tuple.Append[Vec[N - 1], Double] }
面白いことにこうするとVec[4]
と書くだけで(Double, Double, Double, Double)
が表現できる。でもこのコードを書いている間は本当にそうなっているか不安なので、summon
で確かめることができる:
summon[Vec[4] =:= (Double, Double, Double, Double)] // コンパイルが通る summon[Vec[4] =:= (Double, Double)] // コンパイルが通らない
using
できることを確認する
もう一つの典型的なユースケースは、CirceなどのJSONライブラリにおけるEncoder
やDoobieなどのDBライブラリにおけるMeta
といった、何かを変換するために必要なインスタンスを、using
で呼び出せるか確認することだ。
つまり、どういうわけかcase class Foo(b: Bar)
がJSONになってくれない、おかしいなあ、といったときにEncoder[Foo]
やEncoder[Bar]
があることをサッと確認する、といった流れで使う。
import io.circe._, io.circe.generic.auto._ case class Foo(n: Int) // ... summon[Encoder[Foo]] // Encoder[Foo]をusingできるならコンパイルが通る
というか、using
できるはずのものをあえて明示的に召喚することからsummonという名前がついているのだ。
ちなみにsummon
の定義はシンプルそのもの。
/** Summon a given value of type `T`. Usually, the argument is not passed explicitly. * * @tparam T the type of the value to be summoned * @return the given value typed: the provided type parameter */ transparent inline def summon[T](using x: T): x.type = x
=:=
の仕組み
賢い読者はこう思ったに違いない。
using
できるものを明示的に呼び出しているのは分かったけど、じゃあ型が一致してるかどうかをsummon[A =:= B]
で判定できるのはどうしてなんだよ?
その種明かしをしよう。=:=
は、実は=:=[A, B]
という形で定義されたクラスで、A
とB
とが等しい場合にのみインスタンスが導出されるような巧妙な仕掛けになっている。かなり巧妙*1なのでいったんここでは踏み込まない。ちなみに<:<
も同様の仕組みになっている。
まぁ今回は使い方を説明するということで、なんとか・・・
*1:これだけで1記事必要