Polyfunctionでpartially applied typeを改善する
scala3で登場したpolyfunctionを用いてscala2系で実現されていたpartially applied typeを改善する。
問題設定
例えばリテラル型を幽霊型として用いて、長さの情報を型レベルで保持するSizedListを考えてみよう。
import scala.compiletime.ops.int._
// Lは型レベルのListの長さの情報
final case class SizedList[L <: Int, V](data: List[V]):
def head(using L > 0 =:= true) = data.head
このクラスにはheadというメソッドが生えていて、headは型レベルの長さが1以上のときにのみ呼ぶことができる。
val sizeOne = SizedList[1, Int](List(1))
val sizeZero = SizedList[0, Int](List(0))
sizeOne.head
sizeZero.head
// [error] 20 | sizeZero.head
// [error] | ^
// [error] | Cannot prove that (0 : Int) > (0 : Int) =:= (true : Boolean).
ちなみにheadは幽霊型の有用性を示しただけで特に本題とは関係ない。
またそもそも型レベルの長さが実際のListの長さと違ってもSizedListは作れるがその問題は一旦置いておく。
ここで本題の問題があり、それはSizedListを作る際には必ずSizedList[1, Int]の様にどちらの型パラメタも埋めてあげる必要があるということである。
しかしこれは冗長であり、なぜならLは幽霊型なので自分で埋める必要があるがVはListの中身から推論可能なのでできることならば自分で埋めたくない。
これを解決するのがpartially applied typeである。
2系でのpartially applied typeのやり方
partially applied type、つまり型を部分的に適用するためのテクニックだが2系と同じ様にscala3でも書くと以下のように実装されていた。
object SizedList:
class SizedListPartiallyApplied[L <: Int]:
def apply[V](data: List[V]): SizedList[L, V] = SizedList[L, V](data)
def apply[L <: Int]: SizedListPartiallyApplied[L] =
SizedListPartiallyApplied[L]
これを使うと以下のようにLのみを埋めてSizedListを作ることが可能になる。
val sizeOne = SizedList[1](List(1))
val sizeZero = SizedList[0](List(0))
このテクニックは型パラメタをclassのコンストラクタとそのメソッドに分けることで部分的に型パラメタを埋めることを可能にするものである。
scala3のpolyfunctionを使ったpartially applied type
scala3ではpolyfunctionという機能が追加された。
これは型パラメタを取る関数だと解釈できる。
つまり今まではdefを使ったメソッドにしか許されていなかったことがFunctionでもできるようになったということである。
例えば2系までだと以下のようにメソッドを使用しなければpolymorphicな関数は書けない。
def toListM[V](v: V) = List(v)
val toListF = (v: Int) => List(v)
toListM("a") // => List("a")
toListF(1) // => List(1) Intしか受け付けない
しかし関数にもpolymorphicな力を与えたのがpolyfunctionでscala3では以下のように書くことができる。
def toListM[V](v: V) = List(v)
val toListF = [V] => (v: V) => List(v)
toListM("a") // => List("a")
toListF("a") // => List("a")
toListF(1) // => List(1) 何でも行ける
polyfunctionを使用するとpartially applied typeは以下の様に実装できる
object SizedList:
def apply[L <: Int]: [V] => List[V] => SizedList[L, V] =
[V] => (data: List[V]) => SizedList[L, V](data)
val sizeOne = SizedList[1](List(1))
val sizeZero = SizedList[0](List(0))
2系と違ってわざわざクラスを定義しなくても良くなった。
scala3ではクラスを使用したpartially applied typeは必要ないのか?
残念ながら必要です。
例えば今までコンストラクタでListとして受け取っていたdataを可変長引数で受け取りたい場合クラスだと以下のように書ける。
object SizedList:
class SizedListPartiallyApplied[L <: Int]:
def apply[V](data: V*): SizedList[L, V] = SizedList[L, V](data.toList)
def apply[L <: Int]: SizedListPartiallyApplied[L] =
SizedListPartiallyApplied[L]
SizedList[1](1)
SizedList[2](1, 2)
これはpolyfunctionでは書けない。
なぜならfunctionは可変長引数を受け取れないからである。
// こんな感じのコードはかけない
def apply[L <: Int]: [V] => List[V] => SizedList[L, V] =
[V] => (data: V*) => ???