LoginSignup
0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-29

元々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操作のデファクトスタンダードとして確固たる地位を確立してくれたらうれしいなーと思っています!

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