😀

GO1.18のGenericsを用いてmap,flatMap,filterなどのコレクションライブラリを作ってみる

に公開

はじめに

ついにgo1.18でgenericsが使用できるようになりました.
go.1.17で導入される予定でしたが延期されたので待望のアップデートです.
go1.18 release notes

これによってScalaやKotlin, ypescriptのように, map/reduce/filterなどのコレクション関数が使用できるかもしれません.
早速試してみましょう.

また本記事で紹介しているコレクション関数は以下に公開しております.
go-collections

コレクション関数例

Map

ある型Tのsliceを受け取って,異なる型Vを返却するmap関数です.

func Map[T, V any](elms []T, fn func(T) V) []V {
	outputs := make([]V, len(elms), cap(elms))
	for i, elm := range elms {
		outputs[i] = fn(elm)
	}
	return outputs
}

使用例

数値のsliceを受け取って, 数値を文字列に変換した上でprefixを追加して返却します.
この時点で,今まではinterface{}型で受け取り型アサーションが必要でしたが, それらが不要となりスッキリとしていますね.

サンプルコード
func exampleMap() {
	inputs := []int{1, 2, 3}
	results := col.Map(inputs, func(num int) string {
		return fmt.Sprintf("result_%d", num)
	})
	fmt.Printf("map input slice  :%v\n", inputs)
	fmt.Printf("map results      :%v\n", results)
}
//map input slice  :[1 2 3]
//map results      :[result_1 result_2 result_3]

MapE

エラーを上位にスローする言語では不要な関数ですが, goの場合エラーを戻り値として返却する必要があります.
そこでエラー発生時は処理を中断してエラーを返却する関数を用意します.

func MapE[T, V any](elms []T, fn func(T) (V, error)) ([]V, error) {

	outputs := make([]V, len(elms), cap(elms))
	for i, elm := range elms {
		o, err := fn(elm)
		if err != nil {
			return nil, err
		}
		outputs[i] = o
	}
	return outputs, nil
}

使用例

偶数を渡すとエラーを返却する関数を用意します.
ここに偶数を含んだ数値のSliceを渡すと,処理が中断されエラーが返却されます.

サンプルコード
func exampleMapE() {
	errEven := errors.New("even error")
	inputs := []int{1, 3, 5, 6, 7, 11}
	results, err := col.MapE(inputs, func(input int) (string, error) {
		if input%2 == 0 {
			return "", errEven
		}
		return fmt.Sprintf("output_%d", input), nil
	})
	fmt.Printf("mapE input slice   :%v\n", inputs)
	fmt.Printf("mapE results       :%v\n", results)
	fmt.Printf("mapE error         :%v\n", err)
}
//mapE input slice   :[1 3 5 6 7 11]
//mapE results       :[]
//mapE error         :even error

FlatMap

特定の型TのSliceを受け取って, この関数の戻り値が異なる特定の型VのSliceの場合, つまり戻り値が多次元Sliceとなる場合flatten処理を行った上で返却します.

func FlatMap[T, V any](elms []T, fn func(T) []V) []V {
	outputs := make([]V, 0)
	for _, elm := range elms {
		outputs = append(outputs, fn(elm)...)
	}
	return outputs
}

使用例

inputのusersは内部フィールドにpetsというstringのSliceをもっています.
コレクションに渡す関数では各userのpetsを抽出してflatten処理を行い返却します.

サンプルコード
func exampleFlatMap() {
	type user struct {
		pets []string
	}
	inputs := []user{{pets: []string{"dog", "cat"}}, {pets: []string{"dog", "pig"}}, {pets: []string{"dog"}}}
	results := col.FlatMap(inputs, func(u user) []string { return u.pets })
	fmt.Printf("flatMap input slice   :%+v\n", inputs)
	fmt.Printf("flatMap results       :%v\n", results)
}
//flatMap input slice   :[{pets:[dog cat]} {pets:[dog pig]} {pets:[dog]}]
//flatMap results       :[dog cat dog pig dog]

MapWithIndex

map処理においてループ処理のindexが必要な場合の関数です.
特定の型Tのsliceを受け取って, 関数内ではindexと特定の型Tを引数として受け取り, 異なる型Vで返却します.

func MapWithIndex[T, V any](elms []T, fn func(int, T) V) []V {
	outputs := make([]V, len(elms), cap(elms))
	for i, elm := range elms {
		outputs[i] = fn(i, elm)
	}
	return outputs
}

使用例

nameフィールドを持った型userを用意し, 渡す関数ではindexとuserのnameを連結して返却しています.

サンプルコード
func exampleMapWithIndex() {
	type user struct {
		name string
	}
	users := []user{{name: "AAA"}, {name: "BBB"}}
	results := col.MapWithIndex(users, func(i int, u user) string { return fmt.Sprintf("%d_%s", i, u.name) })
	fmt.Printf("mapWithIndex input slice   :%+v\n", users)
	fmt.Printf("mapWithIndex results       :%v\n", results)
}
//mapWithIndex input slice   :[{name:AAA} {name:BBB}]
//mapWithIndex results       :[0_AAA 1_BBB]

Filter

Sliceに対して,特定の条件のみを抽出する関数です.

func Filter[T any](elms []T, fn func(T) bool) []T {
	outputs := make([]T, 0)
	for _, elm := range elms {
		if fn(elm) {
			outputs = append(outputs, elm)
		}
	}
	return outputs
}

使用例

偶数の数値のSliceを渡し, 偶数のみを抽出して取得します.

サンプルコード
func exampleFilter() {
	inputs := []int{1, 2, 3, 4, 5}
	results := col.Filter(inputs, func(num int) bool {
		return num%2 == 0
	})
	fmt.Printf("filter input slice   :%+v\n", inputs)
	fmt.Printf("filter results       :%v\n", results)
}
//filter input slice   :[1 2 3 4 5]
//filter results       :[2 4]

Uniq

比較可能なSliceを渡すと重複を排除して結果を返す関数です.

func Uniq[T comparable](elms []T) []T {
	outputs := make([]T, 0, len(elms))
	m := make(map[T]bool)
	for _, elm := range elms {
		if _, ok := m[elm]; !ok {
			m[elm] = true
			outputs = append(outputs, elm)
		}
	}
	return outputs
}

comparableは今回追加された型で, == や != を使って比較できる全ての型の集合を表すインターフェース型となっており、型パラメータとしてのみ使用可能です.

※参考

The new predeclared identifier comparable is an interface that denotes the set of all types which can be compared using == or !=. It may only be used as (or embedded in) a type constraint.

https://tip.golang.org/doc/go1.18

使用例

サンプルコード
func exampleUniq() {
	inputs := []string{"a", "b", "b", "c", "c", "c"}
	results := col.Uniq(inputs)
	fmt.Printf("filter input slice   :%+v\n", inputs)
	fmt.Printf("filter results       :%v\n", results)
}
//filter input slice   :[a b b c c c]
//filter results       :[a b c]

GroupByUniq

特定の型TのSliceを渡すと,関数で指定した型Vをkeyとしたmapを返却します.
※(key, value) = (V, T)のmap
key(型V)に対するvalue TはUniqueである前提となっています.

func GroupByUniq[T any, V comparable](elms []T, fn func(T) V) map[V]T {
	outputs := make(map[V]T, 0)
	for _, elm := range elms {
		key := fn(elm)
		if _, ok := outputs[key]; ok {
			continue
		}
		outputs[key] = elm
	}
	return outputs
}

使用例

型userのIDをkeyとして, userがvalueとなるmapを返却します.

サンプルコード
func exampleGroupByUniq() {
	type user struct {
		ID   int
		Name string
	}
	inputs := []user{{ID: 1, Name: "one"}, {ID: 3, Name: "three"}, {ID: 10, Name: "ten"}}
	results := col.GroupBy(inputs, func(u user) int { return u.ID })
	fmt.Printf("groupByUniq input slice   :%+v\n", inputs)
	fmt.Printf("groupByUniq results       :%v\n", results)
}

//groupByUniq input slice   :[{ID:1 Name:one} {ID:3 Name:three} {ID:10 Name:ten}]
//groupByUniq results       :map[1:[{1 one}] 3:[{3 three}] 10:[{10 ten}]]

GroupBy

特定の型TのSliceを渡すと,関数で指定した型Vをkeyとしたmapを返却します.
※(key, value) = (V, []T)のmap
前述のGroupByUniqとの違いは, valueであるTのSliceであることです.

func GroupBy[T any, V comparable](elms []T, fn func(T) V) map[V][]T {
	outputs := make(map[V][]T, 0)
	for _, elm := range elms {
		key := fn(elm)
		if values, ok := outputs[key]; ok {
			outputs[key] = append(values, elm)
			continue
		}
		outputs[key] = []T{elm}
	}
	return outputs
}

使用例

サンプルコード
func exampleGroupBy() {
	type user struct {
		ID      int
		Country string
	}
	inputs := []user{
		{ID: 1, Country: "JP"},
		{ID: 3, Country: "JP"},
		{ID: 10, Country: "US"},
		{ID: 50, Country: "JP"},
	}
	results := col.GroupBy(inputs, func(u user) string { return u.Country })
	fmt.Printf("groupBy input slice   :%+v\n", inputs)
	fmt.Printf("groupBy results       :%v\n", results)
}
//groupBy input slice   :[{ID:1 Country:JP} {ID:3 Country:JP} {ID:10 Country:US} {ID:50 Country:JP}]
//groupBy results       :map[JP:[{1 JP} {3 JP} {50 JP}] US:[{10 US}]]

Chunk

特定の型TのSliceを渡すと, 指定したsizeで分割したSliceを返却します.

func Chunk[T any](elms []T, size int) [][]T {
	total := len(elms)
	if size <= 0 {
		return [][]T{elms}
	}
	if total <= 0 {
		return [][]T{}
	}
	if total <= size {
		return [][]T{elms}
	}
	end := size
	outputs := make([][]T, 0)
	for start := 0; start < total && end <= total; end += min(size, total-end) {
		outputs = append(outputs, elms[start:end])
		start += size
	}
	return outputs
}

使用例

複数の文字列を持つSliceを渡すと, 指定した3つ数に分割したSliceが返却されます.

サンプルコード
func exampleChunk() {
	inputs := []string{"a", "b", "c", "d", "e", "f", "g"}
	results := col.Chunk(inputs, 3)
	fmt.Printf("chunk input slice   :%+v\n", inputs)
	fmt.Printf("chunk results       :%v\n", results)
}
//chunk input slice   :[a b c d e f g]
//chunk results       :[[a b c] [d e f] [g]]

メソッドチェーンで使用したい

他の言語だと以下のように, メソッドチェーンで使用できるかと思います.
Goにもこのような形で利用可能なのでしょうか?

users.filter(u => u.status = VALID)
    .distinct(u => u.id()) 
    .map(u => u.id())

結論から言うと現状は難しいです.
現状のGoのGenericsではメソッドにおいて型パラメータを使用できないため, Sliceに上記のようなコレクション関数を追加することは難しそうです.

type List struct{
  Elms Slice
}

// メソッドにおいて型パラメータは使用できない
func(l List)[T,V any]Map(func(T)V)[]V{
 // 省略
}

これはこれを実現するためにはメソッドにおいてGenerics定義を行うことが必要なためです.
そのため、sliceを引数とするようなコレクション関数は今後goに追加される可能性はありますが、メソッドチェーンを行うような方の追加は今後Generics仕様が変わらないと難しいです.

最後に

いかがでしたでしょうか。
今まで毎回interface{}型で受け取って, 型アサーションを使用していた処理が不要になり, 使用する際もコード量が減ったかと思います.
goのgenerics仕様はこれでfixというわけではなく, 以下のように今後の機能追加も示唆されています。

※公式Relase noteより抜粋

The Go compiler cannot handle type declarations inside generic functions or methods. We hope to provide support for this feature in a future release.

なので今後のリリースも楽しみに待ちましょう!

Discussion