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
追記(2024-09-10)
ところで、Scala 3にはType Lambdasという機能が生えていて、その場で型引数を取るような型を定義して使うことができる。ようするに無名関数の型版だ。
type F[A] = (A, A) type F = [A] =>> (A, A)
例えば上掲の2つの定義は等価だ。これを値レベルで見ると、function
を使って関数を定義するか、const ... = (x) => ...
の形で関数を定義するか、という、TypeScriptとかでおなじみの形になる。
この機能を利用すると、Aux
は以下のように書くこともできる:
object Entity: type Aux = [K] =>> Entity { type Key = K }
このAux
の定義はK
とEntity
だけに依存しているから、EntityKey
の定義の中にそのまま持ち込める:
type EntityKey[T <: Entity] = T match case [k] =>> Entity { type Key = k } => k
しかしこのままではうまく型引数の導入ができないようで、いったん括弧で括ってやる必要があるみたいだ:
type EntityKey[T <: Entity] = T match case ([k] =>> Entity { type Key = k })[k] => k
うーん、ちょっと見た目が微妙になってしまったけれど、定義を削りたいときのためにこういう使い道があることは覚えておくと便利だ。