skiplistmapの発表も終わったし、go 1.18 から type parameter のサポート もはじまったということで 自分のpackage も type parameter に対応していこうかなといじりはじめたら、結構罠があったりしたのでそのメモ代わり
そもそも type parameter の仕様ってはっきり記載されてなくて
たぶん type parameter Proposal しかいまだに見るしかないのだろうか? なんかきちんと記載したものがあるなら誰か教えてほしい。
埋め込みの中のfield の unsafe.Offsetof が取れない
まずはいきなりバグにはまったという話
lock free なlinked list である elist_head とかでは
type SampleEntry struct {
Name string
Age int
ListHead
}
というふうに埋め込んでいる。
定義したリストには 以下のようにmethod をはやす前提だ
type List interface {
Offset() uintptr
PtrListHead() *ListHead
FromListHead(*ListHead) List
}
ここの例でいうと SampleEntry から ListHead へのoffset を unsafe.Offsetof() をつかってとらないといけない
item := SampleEntry{}
unsafe.OffsetOf(item.ListHead)
ここは問題ないのだが
skiplistmap などでは
type MapHead struct {
ListHead
}
type Entry struct {
MapHead
}
とかになっている。このときtype parameter をつかって
type Entry[T any] struct {
Val T
MapHead
}
とかってした場合
entry := new(Entry[int])
unsafe.OffsetOf(&entry.ListHead)
を実行すると0 がかえってきてしまう。
どうやら MapHead からのListHead までのoffset を とるらしい。
ということで bug report した。
あっというまに 1.18backport に入り。1.18.3 に入るかと おもったがどうやら間に合わなかったらしくたぶん 1.18.4に入りそう
pointer でmethod をはやしたやつがなやましい。
type Base struct {
I int
}
type(b *Base) Int() int {
return b.I
}
type Sample[T any] struct {
Value T
}
こんな感じで定義したときに
func Fuga[T any](s Sample[T]) {
s.Value.I // 呼べない
s.Value.Int() // 呼べない
}
こういう感じで Base の Int() とかは呼べない。 T が any だから当然。
てことで
type CallerInt interface{
Int()
}
type Sample[T CallerInt] struct {
Value T
}
これでも呼べない。*Base は CallerInt だけど Base は CallerInt じゃないので
Sample[Hoge]
はcontaraint ではじかれて
Sample[*Hoge]
だと Sample[T] のメンバーがポインタになってしまう。
こんなことを悩んでいたら
僕らの心の親友であるstackoverflow 先生に How can I instantiate a non-nil pointer of type argument with generic Go? こんな記事があった。
type CallerInt[T] struct {
Int()
*T
}
func Fuga[T any, PT CallerInt[T]](s Sample[T]) {
PT(&s.Value).Int()
}
なんていうかどっかでみたtemplate 感があふれてきて、読んでもわからないとか なりそうw
説明すると CallerInt[T] は *T を受けるので PT() してあげると Tのポインタで かつ Int() をもってる状態になるからそれなら呼べるということですね。ポインタでも実体でも method のinterface がというconstraint をつけるにはいまのところこれしか なさそうでこれはなんとかしてほしい感はあるけど難しいかなぁ。
type parameter を使う理由として、interface{} の変換コストがバカにならないので 減らしたいということもある。
しかし このやり方すると PT() のたびに変換はいるし。 制約をいれる時点で interface{} 化してしまうので type any = interface{} だしね
そうすると 単純にinterface{} つかうより constraint で interface{} 変換 PT() と呼ぶところでもう一度実体化と2回走るのでこのあたりはなんとかしてほしい。
method が定義されてるものは method 呼び出しとかはもうちょっと最適化できるだろうと思うがそこはどうなのか
ということで可能な限り constraint を使わないでうまくやるのがパフォーマンスだしつつ恩恵に預かるということで
頭をひねって linked list のtype parameter 版は以下のような感じではじめようかとおもった
Proposal にある
// ListHead is the head of a linked list.
type ListHead[T any] struct {
head *ListElement[T]
}
// ListElement is an element in a linked list with a head.
// Each element points back to the head.
type ListElement[T any] struct {
next *ListElement[T]
val T
// Using ListHead[T] here is OK.
// ListHead[T] refers to ListElement[T] refers to ListHead[T].
// Using ListHead[int] would not be OK, as ListHead[T]
// would have an indirect reference to ListHead[int].
head *ListHead[T]
}
これだと ListElement[T]
に next と head があって両方あって空間効率的にすこし無駄なので
type ListHead struct {
next, prev *ListHead
}
type Element = any
type List[T any] struct {
ListHead
Element T
}
func New[T any]() *List[T] {
start := &List[T]{}
end := &List[T]{}
start.prev = &start.ListHead
start.next = &end.ListHead
end.prev = &start.ListHead
end.next = &end.ListHead
return start
}
func ElementOf[T any, L List[T]](h *listHead) *L {
l := new(List[T])
offset := unsafe.OffsetOf(l.Element)
return (*List[T])(unsafe.Add(unsafe.Pointer(h), offset))
}
こういう感じにすることで List[T] 自体に Prev()/Next() を生やすだけで いままでように 先にmethod を定義するというのは避けられそうな気がしている。