関連型
💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。
関連型(Associated type)とは、トレイトの型メンバであり、各トレイトインスタンスごとに指定されるものです。関連型は、多引数型クラスに代わる、より自然な選択肢と見なされることがよくあります。
例を使って関連型を説明します。
加算できる型のためのトレイトを次のように定義できます:
trait Addable[t] {
pub def add(x: t, y: t): t
}
浮動小数点数、整数、文字列などの型に対して、Addable トレイトの複数のインスタンスを実装できます。例えば、Int32 のインスタンスは次のようになります:
instance Addable[Int32] {
pub def add(x: Int32, y: Int32): Int32 = x + y
}
String のインスタンスは次のとおりです:
instance Addable[String] {
pub def add(x: String, y: String): String = "${x}${y}"
}
しかし、Set に要素を追加したい場合はどうでしょうか?
直感的には、次のように書きたくなります:
instance Addable[Set[a]] with Order[a] {
pub def add(s: Set[a], x: a): Set[a] = Set.insert(x, s)
}
しかし、この add のシグネチャは Addable で宣言されたシグネチャと一致しません。
この問題は、関連型を使って Addable の柔軟性を高めることで解決できます:
trait Addable[t] {
type Rhs
pub def add(x: t, y: Addable.Rhs[t]): t
}
Addable トレイトは Rhs という名前の関連型を持つようになりました。add のシグネチャに見られるように、この関連型は Addable.Rhs[t] として参照します。Addable のインスタンスを宣言するときは、必ず関連型を指定しなければなりません。
これまでどおり、整数や文字列に対するインスタンスも実装できます。例えば:
instance Addable[Int32] {
type Rhs = Int32
pub def add(x: Int32, y: Int32): Int32 = x + y
}
さらに、Set に要素を追加できるインスタンスも実装できます:
instance Addable[Set[a]] with Order[a] {
type Rhs = a
pub def add(s: Set[a], x: a): Set[a] = Set.insert(x, s)
}
重要なのは、_各トレイトインスタンスが関連型を指定する_という点です。
ここで、Set[a] に対して2つのインスタンス、すなわち (a) 上記のように Set に要素を追加するもの、(b) 2つの Set を足し合わせるもの、を指定できるのではないかと考えるかもしれません:
instance Addable[Set[a]] with Order[a] {
type Rhs = Set[a]
pub def add(x: Set[a], y: Set[a]): Set[a] = Set.union(x, y)
}
しかし、それぞれのインスタンスは単独では有効であるものの、両方を同時に持つことはできません:
❌ -- Instance Error --------------------------------------------------
>> Overlapping instances for 'Addable'.
...
このような重複したインスタンスが存在すると、Addable.add(Set#{}, Set#{}) のような式が曖昧になってしまいます。2つの Set を足し合わせているのでしょうか?それとも、空の Set を Set に追加しているのでしょうか?
例: ForEach トレイト
関連型を使って、forEach 関数を持つコレクションのためのトレイトを定義できます:
trait ForEach[t] {
type Elm
pub def forEach(f: ForEach.Elm[t] -> Unit \ ef, x: t): Unit \ ef
}
ここで t はコレクションの型であり、関連型 Elm はその要素の型です。ForEach に対していくつかのインスタンスを実装できます。例えば、List[a] のインスタンスを実装できます:
instance ForEach[List[a]] {
type Elm = a
pub def forEach(f: a -> Unit \ ef, x: List[a]): Unit \ ef = List.forEach(f, x)
}
Map[k, v] のインスタンスも実装できます:
instance ForEach[Map[k, v]] {
type Elm = (k, v)
pub def forEach(f: ((k, v)) -> Unit \ ef, x: Map[k, v]): Unit \ ef =
Map.forEach(k -> v -> f((k, v)), x)
}
興味深く、また有用なのは、要素型をキーと値のペアとして定義できる点です。f の引数にはペアを受け取らせたいので、引数の周りに追加の括弧が必要になります。
String に対しては、個々の文字を1つずつ反復処理できるインスタンスを実装できます:
instance ForEach[String] {
type Elm = Char
pub def forEach(f: Char -> Unit \ ef, x: String): Unit \ ef =
x |> String.toList |> List.forEach(f)
}
例: Collection トレイト
別の例として、コレクションのためのトレイトを定義できます:
trait Collection[t] {
type Elm
pub def empty(): t
pub def insert(x: Collection.Elm[t], c: t): t
pub def toList(c: t): List[Collection.Elm[t]]
}
ここで t はコレクションの型であり、Elm はその要素の型です。すべてのコレクションは、empty、insert、toList という3つの操作をサポートしなければなりません。
Vector[a] に対する Collection のインスタンスを実装できます:
instance Collection[Vector[a]] {
type Elm = a
pub def empty(): Vector[a] = Vector.empty()
pub def insert(x: a, c: Vector[a]): Vector[a] = Vector.append(c, Vector#{x})
pub def toList(c: Vector[a]): List[a] = Vector.toList(c)
}
そして、Set[a] に対する Collection のインスタンスも実装できます:
instance Collection[Set[a]] with Order[a] {
type Elm = a
pub def empty(): Set[a] = Set.empty()
pub def insert(x: a, c: Set[a]): Set[a] = Set.insert(x, c)
pub def toList(c: Set[a]): List[a] = Set.toList(c)
}
等値制約
多相関数を書くとき、関連型を_制限_したい場合があります。
例えば、先ほどの Collection トレイトの例に戻ると、要素型が Int32 であることを要求する関数を書くことができます。これにより、合計を計算する関数を書けるようになります:
def sum(c: t): Int32 with Collection[t] where Collection.Elm[t] ~ Int32 =
Collection.toList(c) |> List.sum
ここで where 節には、_型の等値制約(Type equality constraint)_のリストが含まれています。具体的には、等値制約 Collection.Elm[t] ~ Int32 は、Collection のインスタンスが存在する任意の型 t について、そのインスタンスの要素型が Int32 に等しい限り、sum を使用できることを表明しています。この制限により、コレクションの要素が整数であることが保証され、List.sum を呼び出せるようになります。
デフォルト型
関連型にデフォルト型を定義できます。
Addable に戻ると、関連型 Rhs のデフォルトを t として定義できます:
trait Addable[t] {
type Rhs = t // デフォルト型を持つ関連型
pub def add(x: t, y: Addable.Rhs[t]): t
}
ここでは、インスタンスの実装で Rhs が定義されていない場合、デフォルトで t になることを指定しています。その結果、Int32 のインスタンスを次のように定義できます:
instance Addable[Int32] {
pub def add(x: Int32, y: Int32): Int32 = x + y
}
type Rhs = Int32 を明示的に定義する必要はありません。