概要
Go言語で他言語にあるコレクションを実現してみる企画。この際、Go言語の適正は一旦度外視する。
試行錯誤しつつ頭を整理するための実装を晒しているものなので、正解を知りたい人は別記事を見た方が良い。
実装
まずは、共通処理としてのコレクションを実装してみる。シンプルで利用頻度の高そうな関数はこんな感じだろう。
- contains
- filter
- map1
- foldLeft
- foldRight
- reverse
- headTail
なお、Add
相当の append(slice, elm)
, Get
相当の slice[i]
は既存実装のまま利用するのが最良と考えたのでこららの関数は用意していない。
用意した関数の最後に挙げた
headTail
は 先頭要素のhead
と 次要素移行の tailを同時に返す関数として用意した。Go言語は複数の戻り値をサポートしているので、こういうことができる。片方しか不要なのであればhead, _, _ := c.HeadTail()
という形で不要な方を捨てれば良い。そうは言っても tail用の配列コピーはするので処理効率は推して知るべしである。まぁ気にしない。
package collection
import "github.com/google/go-cmp/cmp"
func contains[T any](l []T, t ...T) bool {
for _, tv := range t {
var found bool
for _, lv := range l {
if cmp.Equal(tv, lv) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
func filter[T any](l []T, f func(t T) bool) []T {
var result List[T]
for _, v := range l {
if !f(v) {
continue
}
result = append(result, v)
}
return result
}
func map1[T, O any](l []T, f func(t T) O) []O {
var result = make(List[O], len(l))
for i, v := range l {
result[i] = f(v)
}
return result
}
func foldLeft[T any](l []T, zero T, f func(t1, t2 T) T) T {
var tmp = zero
for _, v := range l {
tmp = f(tmp, v)
}
return tmp
}
func foldRight[T any](l []T, zero T, f func(t1, t2 T) T) T {
return foldLeft(reverse(l), zero, f)
}
func reverse[T any](l []T) []T {
var tmp = l
for i := 0; i < len(l)/2; i++ {
tmp[i], tmp[len(tmp)-1-i] = tmp[len(tmp)-1-i], tmp[i]
}
return tmp
}
func headTail[T any](l []T) (*T, []T, bool) {
if len(l) == 0 {
return nil, List[T]{}, false
}
return &l[0], l[1:], true
}
次に、List
型を用意し、これらの関数をメソッドとして割り当てていく。List
は追加属性を必要としないので、任意のスライス []T
を型とする。
Embedを用いればもう少しスッキリするが、メソッドではなく関数としての実装を確保しておきたかったので、敢えて利用していない。
package collection
type List[T any] []T
func (l *List[T]) Contains(t T) bool {
return contains[T](*l, t)
}
func (l *List[T]) ContainsAll(t ...T) bool {
return contains[T](*l, t...)
}
func (l *List[T]) Filter(f func(t T) bool) List[T] {
return filter[T](*l, f)
}
func (l *List[T]) Map(f func(t T) T) List[T] {
return map1[T, T](*l, f)
}
func (l *List[T]) FoldLeft(zero T, f func(t1, t2 T) T) T {
return foldLeft[T](*l, zero, f)
}
func (l *List[T]) FoldRight(zero T, f func(t1, t2 T) T) T {
return foldRight[T](*l, zero, f)
}
func (l *List[T]) Reverse() List[T] {
var tmp List[T] = reverse(*l)
return tmp
}
func (l *List[T]) HeadTail() (*T, List[T], bool) {
return headTail(*l)
}
実際に利用する場合はこんな感じ。
func TestContains(t *testing.T) {
var l List[int] = []int{1, 2, 3}
assert.True(t, l.Contains(2))
assert.False(t, l.Contains(4))
}
まとめ
Generics を利用すれば、Go言語と言えどコレクションっぽい実装を実現することができる。あまり Go言語らしい実装ではないので、Goらしさを表に出したければ今回の様にメソッドとして利用するのではなく、共通関数として利用するなり、従来どおりにベタな実装に落とせば良いと思う。
業務に活かせるかは別として仕組みとしてはそれなりの実現が可能なのが分かった。ただ実装していて厳しい点もあった。
- structに型パラメータを指定すると、そのメソッドは追加の型パラメータを指定できないという制約
- 自然と関数を利用することになるので、アロー関数式が無いことによる表現の冗長さ
勘違いしている部分や個人的な意見等あればツッコミお願いします。