Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

関連型

💡 お知らせ: このドキュメントは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 はその要素の型です。すべてのコレクションは、emptyinserttoList という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 を明示的に定義する必要はありません。