元々RailsからGoの案件にうつって一ヶ月半ほど経過しました。
この間に、Rubyに存在した
- 配列から条件にマッチする要素を取得する
find(detect)
- 配列から条件にマッチする要素で絞り込んだ配列を生成する
filter(select)
- 配列から新たな配列を生成する
map
といった便利メソッドが標準ライブラリとして存在せずforループやら活用して自分たちでなんとかするしかない・・・というのが実情であると知って絶望 していましたが、Go 1.18で追加されたジェネリクスを利用した外部ライブラリsamber/loを使えば楽に書けることを知って歓喜しました。
自分と同じようにRails案件からGo案件に移ってきて絶望した人向けに、サンプルコードで対比して紹介したいと思います。
また、このライブラリには弱点も存在するので、その辺も交えて紹介したいと思います。
なお、Rubyでは配列は「Array」と呼び、Goでも配列はあるけど、実際使うのはほぼ動的配列である「Slice」の方・・・ということで、この記事ではこれらの総称として「Collection」と呼ばせていただくことにします。
1. samber/loライブラリ関数の紹介
残念ながら、全部紹介してるとキリがないので、比較的よく使うであろう一部の関数に絞ってサンプルコード付きで紹介します。
- Find()
- Filter()
- Map()
- FilterMap()
- Uniq()
- GroupBy()
他にどんな関数があるか?はこの辺を眺めるとよいかと思います。
特に、Railsエンジニア向けに動作のイメージがつきやすいよう、同様の処理となるRubyのコードも対比として載せておきます。
サンプルコードはそれぞれ動作確認済みです。
結果を見たい場合は以下のサイトにサンプルコードを貼り付けて実行してみてください。
1.1. lo.Find(): 条件にマッチする最初の要素を取得
Ruby: find(detect)
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
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)
sample_list = [1, 2, 3]
# 奇数で絞り込み
filterd_list = sample_list.filter do |sample|
# マッチ条件
sample % 2 == 1
end
p filterd_list
Go: Filter
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
sample_list = [1, 2, 3]
# 要素の値を2倍に
converted_list = sample_list.map do |sample|
# 置換後の要素値とマッチ条件
sample * 2
end
p converted_list
Go: Map
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
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
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
sample_list = [1, 2, 3]
# 偶数・奇数でグルーピング
grouped_map = sample_list.group_by do |sample|
# キー
sample % 2
end
p grouped_map
Go: GroupBy
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ものの、未だ解決はされていません。
3. あとがき
標準ライブラリにもsamber/lo的な便利関数ができてくれないものか・・・もしくはsamber/lo自体が標準ライブラリになってくれないかと思っています。
この記事が多くの人の目に触れて、samber/loにもっともっとStarがついてCollection操作のデファクトスタンダードとして確固たる地位を確立してくれたらうれしいなーと思っています!