前までGo言語でFilter的なことをやりたい時はgo-funk
を使っていたが、gen
を使うようになってからコードが読みやすくなったように感じるのでオススメしたい。
方法1: go-funkを使う
go-funkというライブラリを使うと、自前のstructのsliceに対してもFilterやMapなどの処理をかける。
result := funk.Filter(books, func(book *Book) bool {
return book.Author == "John Smith"
})
Filter以外にもFind
やMap
、Uniq
など非常に便利な関数が用意されている。手軽さを重視するなら、go-funk
を使うのも悪くないとは思う。
ただし、go-funkはリフレクションの仕組みを使って実装されているため、返り値がinterface{}
型のsliceになっているため毎回キャストが必要になったり、ビルド時の型チェックが効かないといったデメリットがある。
books := []*Book{
{Author: "John Smith", Title: "My life"},
{Author: "John Smith", Title: "Money"},
{Author: "Paul Brown", Title: "Your life"},
}
titles := funk.Map(funk.Filter(books, func(b *Book) bool {
return b.Author == "John Smith"
}), func(b *Book) string {
return b.Title
}) // []interface{}型になってしまう
ビルド時の型チェックが通らないことが問題で、例えばこんなコードを書いたとしてもビルドできてしまう。
// booksは*Bookのスライスなのに、判定用の関数の引数は*Songになっている
titles := funk.Filter(books, func(s *Song) bool {
return song.Name == "Your song"
})
手軽で便利なのだが、使っているとこのやり方はGo的ではないように感じる。
また、FilterもMapも関数の引数に配列を渡す形になるため、入れ子構造になって読みにくい。できれば下記のように書きたい。
titles := books.Filter(...).Map(...)
方法2: genを使う
go-funkは気軽に使えるという点では良いのだが、もっと無理なくGoらしくFilterを実現する方法がないか調べていたところ、genに辿り着いた。genはコードジェネレータなので、好みが分かれるかもしれないが、生成されるコードはビルド時の型チェックが有効になるし読みやすいので気に入っている。
genのコード生成を使ってFilter(Where)などのメソッドを生成すると下記のようなコードでFilterやMapを書けるようになる。
インストール
go get github.com/clipperhouse/gen
アノテーションを書く
// +gen * slice:"Where"
type Book struct {
Author string
Title string
}
コード生成したい型の上に+gen
から始まるアノテーションを書く。今回はスライス用のコード生成の機能を使っているが、gen
には他のコード生成の機能も用意されている(があまり積極的に開発・利用されてなさそう)。
slice:Where
は、スライス用のジェネレータを使って、Where
メソッド用のコードを含める、という指示になっている。Where以外にも生成可能なメソッドは色々あるが、今回は使わないので省略した。gen: slice typewriter
+gen
とslice:
の間に*
を書いているのは、スライスの要素をポインタでの参照にしたいため。これを書かないと、生成されるスライスBookSlice
は[]Book
となり、書くと[]*Book
となる。
生成する
gen
型名_slice.gen
というファイルが生成される。中には型名Slice
という型と、メソッドWhere
が定義されている。
// BookSlice is a slice of type *Book. Use it where you would use []*Book.
type BookSlice []*Book
// Where returns a new BookSlice whose elements return true for func. See: http://clipperhouse.github.io/gen/#Where
func (rcv BookSlice) Where(fn func(*Book) bool) (result BookSlice) {
for _, v := range rcv {
if fn(v) {
result = append(result, v)
}
}
return result
}
非常にシンプルで難しいこと、複雑なことは何もやっていない。これだけならgen
を使わなくても手動で書いても問題ないが、Where
以外の関数も使い始めるようになったり、あるいは色々な型でこれらの関数を使うようになってくると、やはりコード生成の便利さを感じるようになると思う。
使う
シンプルなコードなので説明するまでもないが、生成されたWhereメソッドを実際に使ってみる。
books := BookSlice{
{Author: "John Smith", Title: "My life"},
{Author: "John Smith", Title: "Money"},
{Author: "Paul Brown", Title: "Your life"},
}
result := books.Where(func(b *Book) bool {
return b.Author == "John Smith"
})
このWhere関数は、この型(Book)のスライスの関数なので、ビルド時の型チェックが有効であり、また返り値もinterface{}
型のスライスではなく、オリジナルの型のスライスとなる。
生成されたコードをさらに拡張する
これはgen
の機能ではないのだが、gen
のSlice typewriter
で生成されたSlice
に、さらに自前で使いたいメソッドを追加で実装するとより便利になる。
自分の場合は型名_slice_ext.go
というファイルを作成し、その中で型Slice
に対してさらに良く使う機能を関数で実装している。
func (rcv BookSlice) Titles() []string {
titles := make([]string, len(rcv))
for i, b := range rcv {
titles[i] = b.Title
}
return titles
}
こうすることで、genが生成するメソッドと組み合わせて下記のような書き方ができる。
titles := books.Where(func(b *Book) bool {
return b.Author == "John Smith"
}).Titles()
型チェックが効き、それでいて読みやすくGo的で気に入っている。
結論
コード生成については好き嫌いが分かれるところだが、Goの言語自体にコード生成を補助する機構が用意されていることや、静的解析によるコンパイル時の型チェックの有効性などを考えると、gen
のslice typewriter
を使うという選択肢は割とありだと思う。実際に、仕事で多用するようになってから、go-funk
を多用していた頃よりもコードが読みやすく書きやすくなったと感じている。エディタからショートカットでgen
コマンドを呼び出せるようにしておいたりすると、ますます便利なのでぜひ試してみてほしい。