Scala 2ではこういう表現がたまにあった:
trait Entity { type Key } type KeyOfEntity[E <: Entity] = E#Key
ねぇよそんなもん、と思った方はここで終わりです。せっかくなので聞いていってください。
KeyOfEntity
はEntity
を受け取って、その内部のKey
型を取り出す。Scalaでは、A#X
と書くと型A
の中の型X
を表現できるのだ。ちょうどa.x
と書くとオブジェクトa
のプロパティx
を表現するのと同じ感じだ。DDDとかやってるとこういう型操作がたまに出現する。Scalaはこういうふうに型をそのまま値みたいに扱えるのでおもろいですよね。
このように抽象的な型を取り出す操作をGeneral Type Projectionと言うようだ。
しかしScala 3からはGeneral Type Projectionは廃止されているので、このままではコンパイルエラーが出て動作しない。
Match Types / Auxパターン
Scala 3からはMatch TypesとAuxパターンを使ってこのような操作を表現するようになった。順に見ていこう。
Auxパターン
以下のコードを見てほしい。
object Entity: type Aux[K] = Entity { type Key = K }
Aux
は、新たに型パラメータK
を導入し、内部でKey = K
という束縛を行っている。こうすることで、Key
は型パラメータK
に持ち上げられて取り出せるようになる。このように、型パラメータを導入して内部で束縛することで、内部の型を型パラメータとして取り出すというパターンはAuxパターンとよく呼ばれる。具体的な使い方としては、コンパニオンオブジェクトにAux
という型定義を用意することが多い。どこで使われるかというと例えばジェネリック操作ライブラリShapelessの内部で使われていたりする。
Match Types
Match Typesとは型レベルでmatch
式が使えるというScala 3の大変便利な機能である。
type Even type Odd type EvenOrNot[B] = B match case true => Even case false => Odd // この型はOddになるはず type X = EvenOrNot[false] // summonを使って型が合致するかを確認する。=:=は左右の型が等しいときにのみsummonが通る summon[X =:= Odd] // コンパイルが通る summon[X =:= Even] // コンパイルが通らない
こうして見るとパラメータ付きの型、Java風に言うとジェネリクスは、型レベルの関数でしかないのだな、という気付きを得られる。
合体
で、Match TypesをどうAuxと組み合わせるかというと、次のようにする:
type EntityKey[T <: Entity] = T match case Entity.Aux[k] => k
こうすると、EntityKey[_]
にEntity
を入れたとき、Aux
パターンで中身のKey
が取り出され、それが返される、という型計算ができる。
trait Foo extends Entity { type Key = Int } type KeyOfFoo = EntityKey[Foo] // => Int
よかったですね。型が取り出せないときはAuxとmatch、覚えておくと一生に一度くらいは役立つかもしれません。
経緯
Scala2で書かれたコードを手直ししていて、以下のようなコンパイルエラーが出るようになり困って調べていたところ次のリンクを見付けたのでメモしたという次第。
Foobar is not a legal path since it is not a concrete type