制約におけるレシーバーの使用

最初に試すアプローチは、Compare メソッドを持つ普通のインターフェースを定義することです:

type Comparer interface {
    Compare(Comparer) int
}

しかし、これがうまく動作しないことにすぐに気づきます。このインターフェースを実装するには、メソッドのパラメータ自体が Comparer でなければなりません。これは実装でパラメータを自身の型にtype assertしなければならないだけでなく、すべての型が Comparer 型を名前で明示的に参照するパッケージを参照しなければならないことも意味します(そうでなければメソッドシグネチャが同一になりません)。これはあまり直交的ではありません。

より良いアプローチは、Comparer インターフェース自体をジェネリックにすることです:

type Comparer[T any] interface {
    Compare(T) int
}

この Comparer は、Comparer がインスタンス化される可能性のある各型に対して、インターフェースの全ファミリーを記述します。Comparer[T] を実装する型は「私は T と自分自身を比較できる」と宣言します。例えば、time.Time は一致する Compare メソッドを持っているため、Comparer[time.Time] を自然に実装します:

// Comparer[Time]を実装
func (t Time) Compare(u Time) int

これは良くなりましたが、まだ十分ではありません。私たちが本当に望んでいるのは、型パラメータが_自分自身_と比較できることを示す制約です。制約が自己参照的であることを望んでいます。微妙な洞察は、自己参照的側面がインターフェース定義自体の一部である必要がないということです。具体的には、Comparer 型の T の制約は単に any です。代わりに、これは MethodTree の型パラメータの制約として Comparer を使用する方法の結果です:

// MethodTreeのゼロ値は使用可能な空のツリーです。
type MethodTree[E Comparer[E]] struct {
    root *methodNode[E]
}

func (t *MethodTree[E]) Insert(element E) {
    t.root = t.root.insert(element)
}

type methodNode[E Comparer[E]] struct {
    value E
    left  *methodNode[E]
    right *methodNode[E]
}

func (n *methodNode[E]) insert(element E) *methodNode[E] {
    if n == nil {
        return &methodNode[E]{value: element}
    }
    sign := element.Compare(n.value)
    switch {
    case sign < 0:
        n.left = n.left.insert(element)
    case sign > 0:
        n.right = n.right.insert(element)
    }
    return n
}

time.TimeComparer[time.Time] を実装しているため、このコンテナの有効な型引数となり、空のコンテナとしてゼロ値を使用できます:

var t MethodTree[time.Time]
t.Insert(time.Now())

完全な柔軟性のため、ライブラリは3つのAPIバージョンすべてを提供できます。重複を最小限にしたい場合、すべてのバージョンが共有実装を使用できます。最も汎用的な関数バージョンをそれに使用できます:

type node[E any] struct {
    value E
    left  *node[E]
    right *node[E]
}

func (n *node[E]) insert(cmp func(E, E) int, element E) *node[E] {
    if n == nil {
        return &node[E]{value: element}
    }
    sign := cmp(element, n.value)
    switch {
    case sign < 0:
        n.left = n.left.insert(cmp, element)
    case sign > 0:
        n.right = n.right.insert(cmp, element)
    }
    return n
}

// Eがcmp.Orderedを実装している場合、要素をツリーに挿入します。
func (t *Tree[E]) Insert(element E) {
    t.root = t.root.insert(cmp.Compare[E], element)
}

// 提供された比較関数を使用して、要素をツリーに挿入します。
func (t *FuncTree[E]) Insert(element E) {
    t.root = t.root.insert(t.cmp, element)
}

// EがComparer[E]を実装している場合、要素をツリーに挿入します。
func (t *MethodTree[E]) Insert(element E) {
    t.root = t.root.insert(E.Compare, element)
}

ここで重要な観察点は、共有実装(関数ベースのバリアント)が何も制約されていないことです。共通のコアとして機能するために、最大限に柔軟でなければなりません。また、比較関数を構造体フィールドに格納しません。代わりに、関数引数の方がコンパイラが分析しやすいため、パラメータとして渡します。

もちろん、まだある程度のボイラープレートが必要です。すべてのエクスポートされた実装は、わずかに異なる呼び出しパターンで完全なAPIを複製する必要があります。しかし、この部分は書くのも読むのも簡単です。