高カインド型
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
Flix は高カインド型(Higher-kinded types)をサポートしています。そのため、トレイトは*型コンストラクタ(Type constructor)*を抽象化できます。
例えば、t[a] という形をした任意のコレクションに対する反復処理を表現するトレイトを書くことができます。ここで、t はカインド Type -> Type の型コンストラクタであり、a はカインド Type の要素型です:
trait ForEach[t: Type -> Type] {
pub def forEach(f: a -> Unit \ ef, x: t[a]): Unit \ ef
}
高カインド型を使うには、Flix はカインド注釈(Kind annotation)を書くことを要求します。つまり、ForEach が型コンストラクタを抽象化していることを Flix に伝えるために、t: Type -> Type と書く必要がありました。
ForEach トレイトのインスタンスを Option に対して実装できます:
instance ForEach[Option] {
pub def forEach(f: a -> Unit \ ef, o: Option[a]): Unit \ ef = match o {
case None => ()
case Some(x) => f(x)
}
}
また、List に対するインスタンスも実装できます:
instance ForEach[List] {
pub def forEach(f: a -> Unit \ ef, l: List[a]): Unit \ ef = List.forEach(f, l)
}
Flix のカインド
Flix は以下のカインドをサポートしています:
Type: Flix の型のカインド。- 例:
Int32、String、List[Int32]。
- 例:
RecordRow: レコードで使われる行のカインド。- 例:
{x = Int32, y = Int32 | r}において、型変数rはカインドRecordRowを持ちます。
- 例:
SchemaRow: 第一級 Datalog 制約で使われる行のカインド。- 例:
#{P(Int32, Int32) | r}において、型変数rはカインドSchemaRowを持ちます。
- 例:
Flix は通常、カインドを推論できます。例えば、次のように書くと:
def sum(r: {x = t, y = t | r}): t with Add[t] = r#x + r#y
t: Type と r: RecordRow というカインドが自動的に推論されます。
次のように明示的に指定することもできます:
def sum[t: Type, r: RecordRow](r: {x = t, y = t | r}): t with Add[t] = r#x + r#y
しかし、このスタイルは慣用的とは見なされません。
Flix が明示的なカインド注釈を要求するのは、次の 4 つの状況です:
- enum 上の、
Type以外のカインドを持つ型パラメータ。 - 型エイリアス上の、
Type以外のカインドを持つ型パラメータ。 - トレイト上の、
Type以外のカインドを持つ型パラメータ。 - トレイト内の、
Type以外のカインドを持つ型メンバ。
カインド注釈が必要になる最も一般的なシナリオは、型パラメータや型メンバがエフェクトを取りうるようにしたい場合です。
高カインド型と関連型の比較
実際のところ、高カインド型と関連型は、よく似た抽象を定義するために使うことができます。
例えば、これまで見てきたように、ForEach トレイトは 2 つの異なる方法で定義できます。
高カインド型を使う方法:
trait ForEach[t: Type -> Type] {
pub def forEach(f: a -> Unit \ ef, x: t[a]): Unit \ ef
}
そして、関連型を使う方法:
trait ForEach[t] {
type Elm
pub def forEach(f: ForEach.Elm[t] -> Unit \ ef, x: t): Unit \ ef
}
ForEach の場合は、関連型を使った定義の方が柔軟です。関連する要素型を Char として、String に対するインスタンスを実装できるからです。しかし、高カインド型も依然として有用です。例えば、Flix 標準ライブラリは Functor トレイトを次のように定義しています:
trait Functor[m : Type -> Type] {
pub def map(f: a -> b \ ef, x: m[a]): m[b] \ ef
}
注目すべきは、m のカインドによって、すべての Functor 実装が構造を保存することが保証される点です。つまり、例えば Option[a] に対して map を適用すると、必ず Option[b] が返ってくることが分かります。