LoginSignup
0

posted at

updated at

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

元々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)

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ものの、未だ解決はされていません。

3. あとがき

標準ライブラリにもsamber/lo的な便利関数ができてくれないものか・・・もしくはsamber/lo自体が標準ライブラリになってくれないかと思っています。

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

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
What you can do with signing up
0