[Go]Collection操作が楽楽書けるsamber/loライブラリの紹介

元々RailsからGoの案件にうつって一ヶ月半ほど経過しました。
この間に、Rubyに存在した

- 配列から条件にマッチする要素を取得する``find(detect)``
- 配列から条件にマッチする要素で絞り込んだ配列を生成する``filter(select)``
- 配列から新たな配列を生成する``map``

といった**便利メソッドが標準ライブラリとして存在せずforループやら活用して自分たちでなんとかするしかない・・・というのが実情であると知って絶望** していましたが、Go 1.18で追加されたジェネリクスを利用した外部ライブラリ[samber/lo](https://github.com/samber/lo)を使えば楽に書けることを知って歓喜しました。

自分と同じように**Rails案件からGo案件に移ってきて絶望した人向けに、サンプルコードで対比して紹介**したいと思います。
また、このライブラリには弱点も存在するので、その辺も交えて紹介したいと思います。

なお、Rubyでは配列は「Array」と呼び、Goでも配列はあるけど、実際使うのはほぼ動的配列である「Slice」の方・・・ということで、この記事ではこれらの総称として「Collection」と呼ばせていただくことにします。

# 1. samber/loライブラリ関数の紹介
残念ながら、全部紹介してるとキリがないので、比較的よく使うであろう一部の関数に絞ってサンプルコード付きで紹介します。
- Find()
- Filter()
- Map()
- FilterMap()
- Uniq()
- GroupBy()

他にどんな関数があるか？は[この辺](https://github.com/samber/lo#-spec)を眺めるとよいかと思います。
特に、Railsエンジニア向けに動作のイメージがつきやすいよう、同様の処理となるRubyのコードも対比として載せておきます。
サンプルコードはそれぞれ動作確認済みです。
結果を見たい場合は以下のサイトにサンプルコードを貼り付けて実行してみてください。
- [Ruby](https://paiza.io/projects/dM9gY7MxHXFXPs-Xa8XYpw?language=ruby)
- [Go](https://go.dev/play/)

## 1.1. lo.Find(): 条件にマッチする最初の要素を取得 

**Ruby: find(detect)**
```find.rb
sample_list = [1, 2, 3]

# 奇数を検索
target = sample_list.find do |sample|
    # マッチ条件
    sample % 2 == 1
end

if target 
    p "exist (#{target})"
else
    p "not exist"
end

# 4の倍数を検索
target2 = sample_list.find do |sample|
    # マッチ条件
    sample % 4 == 0
end

if target2 
    p "exist (#{target2})"
else
    p "not exist"
end
```

**Go: Find**
```find.go
package main

import (
	"fmt"

	"github.com/samber/lo"
)

func main() {
	sampleList := []int{1, 2, 3}

	// 奇数を検索
	target, exist := lo.Find(sampleList, func(sample int) bool {
		// マッチ条件
		return sample%2 == 1
	})

	if exist {
		fmt.Printf("exist (%d)\n", target)
	} else {
		fmt.Println("not exist")
	}

	// 4の倍数を検索
	target2, exist := lo.Find(sampleList, func(sample int) bool {
		// マッチ条件
		return sample%4 == 0
	})

	if exist {
		fmt.Printf("exist (%d)\n", target2)
	} else {
		fmt.Println("not exist")
	}
}
```

## 1.2. lo.Filter(): 条件にマッチする全要素のCollectionを生成

**Ruby: filter(select)**
```filter.rb
sample_list = [1, 2, 3]

# 奇数で絞り込み
filterd_list = sample_list.filter do |sample|
    # マッチ条件
    sample % 2 == 1
end

p filterd_list
```

**Go: Filter**
```filter.go
package main

import (
	"fmt"

	"github.com/samber/lo"
)

func main() {
	sampleList := []int{1, 2, 3}

	// 奇数で絞り込み
	filteredList := lo.Filter(sampleList, func(sample int, _ int) bool {
		// マッチ条件
		return sample%2 == 1
	})

	fmt.Println(filteredList)
}
```

## 1.3. lo.Map(): 要素ごとの処理結果を要素とするCollectionを生成
**Ruby: map**
```map.rb
sample_list = [1, 2, 3]

# 要素の値を2倍に
converted_list = sample_list.map do |sample|
    # 置換後の要素値とマッチ条件
    sample * 2
end

p converted_list
```

**Go: Map**
```map.go
package main

import (
	"fmt"

	"github.com/samber/lo"
)

func main() {
	sampleList := []int{1, 2, 3}

	// 要素の値を2倍に
	convertedList := lo.Map(sampleList, func(sample int, _ int) int {
		// 置換後の要素値とマッチ条件
		return sample * 2
	})

	fmt.Println(convertedList)
}
```

## 1.4. lo.FilterMap(): 条件にマッチする全要素について、要素ごとの処理結果を要素とするCollectionを生成

**Ruby: filter_map**
```filter_map.rb
sample_list = [1, 2, 3]

# 奇数で絞り込み後、要素の値を2倍に
converted_list = sample_list.filter_map do |sample|
    # 置換後の要素値とマッチ条件
    sample * 2 if sample % 2 == 1
end

p converted_list
```

**Go: FilterMap**
```filter_map.go
package main

import (
	"fmt"

	"github.com/samber/lo"
)

func main() {
	sampleList := []int{1, 2, 3}

	// 奇数で絞り込み後、要素の値を2倍に
	convertedList := lo.FilterMap(sampleList, func(sample int, _ int) (int, bool) {
		// 置換後の要素値とマッチ条件
		return sample * 2, sample%2 == 1
	})

	fmt.Println(convertedList)
}
```

## 1.5. lo.Uniq(): 要素の重複を削除したCollectionを生成
**Ruby: uniq**
```
sample_list = [1, 2, 3, 1, 3]
p sample_list

# 重複削除
uniq_list = sample_list.uniq
p uniq_list
```

**Go: Uniq**
```
package main

import (
	"fmt"

	"github.com/samber/lo"
)

func main() {
	sampleList := []int{1, 2, 3, 1, 3}
	fmt.Println(sampleList)

	// 重複削除
	uniqList := lo.Uniq(sampleList)
	fmt.Println(uniqList)
}
```

## 1.6. lo.GroupBy(): 要素ごとの処理結果でグループ化したMapを生成
**Ruby: group_by**
```group_by.rb
sample_list = [1, 2, 3]

# 偶数・奇数でグルーピング
grouped_map = sample_list.group_by do |sample|
    # キー
    sample % 2
end

p grouped_map
```

**Go: GroupBy**
```group_by.go
package main

import (
	"fmt"

	"github.com/samber/lo"
)

func main() {
	sampleList := []int{1, 2, 3}

    // 偶数・奇数でグルーピング
	groupedMap := lo.GroupBy(sampleList, func(sample int) int {
        // キー
		return sample % 2
	})
	fmt.Println(groupedMap)
}
```

# 2. 弱点
非常に便利な``samber/lo``ですが、以下の弱点があります(2022/12/29時点)。

**①一部の関数の実装でパフォーマンスの意識が甘い**
**②繰り返し処理でエラーが発生した場合、それを返すことができない**

## ①一部の関数の実装でパフォーマンスの意識が甘い
例えば、``Filter()``の内部実装は以下のようになっています。

```
// Filter iterates over elements of collection, returning an array of all elements predicate returns truthy for.
// Play: https://go.dev/play/p/Apjg3WeSi7K
func Filter[V any](collection []V, predicate func(item V, index int) bool) []V {
	result := []V{}

	for i, item := range collection {
		if predicate(item, i) {
			result = append(result, item)
		}
	}

	return result
}
```

スライスの初期化が
```
result := []V{}
```
となっていますが、Filter関数の性質上、「Filter後の要素数」は「元の要素数」を超えないことが自明です。
したがってパフォーマンス的にはキャパシティの設定をすべきであり、
```
result := make([]V, 0, len(collection))
```
とするのが妥当と思われます(書籍「実用Go言語」を読んだ感じでは)。


## ②繰り返し処理でエラーが発生した場合、それを返すことができない
Goでは、Rubyのような``try ~ catch``が使えず、**エラーが発生しうる関数・メソッド内でエラーが発生した場合、愚直に呼び出し元に返す** 実装になります。

Collection系の便利関数に設定したい繰り返し処理(``func()``)は、エラーが発生しうるような処理であることもあるでしょう。
しかし、そのようなケースに対応できないのが現状です。

この問題についてはすでにIssueが立っている[Error handling variants for iteratees](https://github.com/samber/lo/issues/82)ものの、未だ解決はされていません。


# 3. あとがき
標準ライブラリにも[samber/lo](https://github.com/samber/lo)的な便利関数ができてくれないものか・・・もしくはsamber/lo自体が標準ライブラリになってくれないかと思っています。

この記事が多くの人の目に触れて、[samber/lo](https://github.com/samber/lo)にもっともっとStarがついてCollection操作のデファクトスタンダードとして確固たる地位を確立してくれたらうれしいなーと思っています！