制約におけるレシーバーの使用
最初に試すアプローチは、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.Time
が Comparer[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を複製する必要があります。しかし、この部分は書くのも読むのも簡単です。