Scala 2 で定番だった Partially-Applied Type イディオムを Scala 3 で簡素化する解説。
結論
こんな感じの Scala 2 コードが、、、
final class PurePartiallyApplied[F[_]](val dummy: Boolean = true ) extends AnyVal {
def apply[A](value: A)(implicit F: Applicative[F]): OptionT[F, A] =
OptionT(F.pure(Some(value)))
}
def pure[F[_]]: PurePartiallyApplied[F] = new PurePartiallyApplied[F]
Scala 3 でこんな風に書ける。
def pure[F[_]]: Applicative[F] ?=> [A] => A => OptionT[F, A] =
[A] => (a: A) => OptionT(a.some.pure)
動機
関数の部分適用のように、型パラメータを複数持つ値の生成時にも、型アノテーションを必要最小限の部分指定に留めてできるだけ型推論させたいが、Scala 2 までは言語仕様上これが無理だった。
そのため Partially-Applied Type イディオムといった工夫が編み出され、特に Cats などで多用されてきたが、使用側では無駄な型アノテーションは減ったものの、定義側ではボイラープレートが増えることになった。
幸い Scala 3 からは、メソッドだけではなく関数も polymorphic に定義できるようになるなど実装の選択肢が増えた。この記事では、Scala 3 の機能を利用して、Partially-Applied Type の定義側も簡潔に書く方法を示してみる。
今まで
Cats の Guideline で説明に使われている OptionT[F, A]
の生成コードをお題とする。
Partially-Applied Type イディオム以前の原始的なコードは以下のようなものだった。
def pure[F[_], A](a: A)(implicit F: Applicative[F]): OptionT[F, A] =
OptionT(F.pure(Some(a)))
pure[List, Int](1) // OptionT[List, Int] = OptionT(List(Some(1)))
簡単に書けるし動くことは動くが、実引数 1
で型が分かるのに更に Int
を明示するのが微妙に無駄に見える。
Partially-Applied Type イディオムでは、型パラメータをクラス定義と apply
に別けて置くことで、「型の部分適用」を実現する。
final class PurePartiallyApplied[F[_]](val dummy: Boolean = true) extends AnyVal {
def apply[A](value: A)(implicit F: Applicative[F]): OptionT[F, A] =
OptionT(F.pure(Some(value)))
}
def pure2[F[_]]: PurePartiallyApplied[F] = new PurePartiallyApplied[F]
pure2[List](1) // OptionT[List, Int] = OptionT(List(Some(1)))
とはいえ、もともとは実質一行だったメソッド定義に比べて大幅にボイラープレートが増えている。以下、これを Scala 3 の言語機能で簡素化する。
改良
段階的に変形する。まず準備として Scala 3 と Cats の syntax でノイズを減らしておく1。
final class PurePartiallyApplied3[F[_]]:
def apply[A](value: A)(using Applicative[F]): OptionT[F, A] =
OptionT(value.some.pure)
def pure3[F[_]]: PurePartiallyApplied3[F] = PurePartiallyApplied3[F]
次に Scala 3 の Context Functions で、Applicative[F]
型の context parameter を取る関数を返すように変更する。
final class PurePartiallyApplied4[F[_]]:
def apply[A](value: A): Applicative[F] ?=> OptionT[F, A] =
OptionT(value.some.pure)
def pure4[F[_]]: PurePartiallyApplied4[F] = PurePartiallyApplied4[F]
さらに Scala 3 の Polymorphic Function Types で、型パラメータ[A] も値域側に移動する。ついでにメソッドから関数に変えておく。
final class PurePartiallyApplied5[F[_]]:
val pure: Applicative[F] ?=> [A] => A => OptionT[F, A] =
[A] => (a: A) => OptionT(a.some.pure)
def pure5[F[_]: Applicative]: [A] => A => OptionT[F, A] = PurePartiallyApplied5[F].pure
この時点ですでに PurePartiallyApplied
クラスの必要性が無いので、1個のメソッドにまとめられる。
def pure[F[_]]: Applicative[F] ?=> [A] => A => OptionT[F, A] =
[A] => (a: A) => OptionT(a.some.pure)
参考
-
フットプリント等のパフォーマンス要素は一旦考慮から外して、ここで
AnyVal
と dummy パラメータを除いた。 ↩