LoginSignup
0
0

More than 1 year has passed since last update.

Go言語で Collectionを用意してみる

Last updated at Posted at 2022-07-31

概要

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に型パラメータを指定すると、そのメソッドは追加の型パラメータを指定できないという制約
  • 自然と関数を利用することになるので、アロー関数式が無いことによる表現の冗長さ

勘違いしている部分や個人的な意見等あればツッコミお願いします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0