genでGolangのslice操作用のメソッドを生やすのが便利だった
コレクション操作、筋肉
golang はシンプルなところがいいところなのですが、
同時に何を書くにもコードをモリモリ書く筋肉が必要になって辛いところがあります。
代表的なところはコレクション操作部分ですね。
type User struct {
ID int
Name string
Age int
Area AREA
}
type AREA string
const (
AREA_TOKYO AREA = `Tokyo`
AREA_CHIBA AREA = `China`
AREA_KANAGAWA AREA = `Kanagawa`
)
type Users []User
func TestUser1(t *testing.T) {
userList := Users{
User{ID: 1, Name: "Hoge", Age: 24, Area: AREA_TOKYO, Score: 200},
User{ID: 2, Name: "Fuga", Age: 14, Area: AREA_CHIBA, Score: 600},
User{ID: 3, Name: "Piyo", Age: 34, Area: AREA_TOKYO, Score: 300},
User{ID: 4, Name: "Foo", Age: 64, Area: AREA_KANAGAWA, Score: 450},
User{ID: 5, Name: "Bar", Age: 32, Area: AREA_KANAGAWA, Score: 200},
User{ID: 6, Name: "HogeX", Age: 20, Area: AREA_KANAGAWA, Score: 300},
User{ID: 7, Name: "FugaX", Age: 46, Area: AREA_CHIBA, Score: 700},
User{ID: 8, Name: "PiyoX", Age: 46, Area: AREA_KANAGAWA, Score: 400},
}
sort.Slice(userList, func(i, j int) bool {
return userList[i].Age > userList[j].Age
})
kanagawaHighScoreUserIds := []int{}
for _, user := range userList {
if user.Area == AREA_KANAGAWA && user.Score >= 300 {
kanagawaHighScoreUserIds = append(kanagawaHighScoreUserIds, user.ID)
}
}
assert.Equal(t, 8, kanagawaHighScoreUserIds[1] , `2位の人はidが8のはず`)
}
Scoreを300以上持っていて、AreaがKanagawaなUserを年齢順に並べて、という処理を特に超愚直に書いてみました。
(この例自体もsliceを破壊したりsort.Searchでちょっぴりマシに使えたりとかはあるのですが)
まだこのくらいの例だといいですが、もう少し複雑になってくるとすぐに「コレクション操作のための一時変数」やfor文がスコープに溢れがちになったりします。
本質的じゃないところで可読性が悪くなるのは結構注意しなければならないところです。
意味の塊を切るために無理やりこういうことしたりもする..
oldest3UsersLiveInKanagawa := func() []User {
filterdUsers := []User{}
sort.Slice(userList, func(i, j int) bool {
return userList[i].Age > userList[j].Age
})
for _, user := range userList {
if len(filterdUsers) >= 3 {
break
}
if user.Area == AREA_KANAGAWA {
filterdUsers = append(filterdUsers, user)
}
}
return filterdUsers
}()
要するにLinqっぽいのが欲しい
「操作のための操作」にはあまり関心がないことが多く、C# でいうところのLinqであったり、jsのmapやfilter、perlのgrep等が使いたかったりするわけです。
Genericsの導入が予定されている2系では該当ライブラリがすぐ出てくるとは思いますが、golang1系の現在はそうもいかず、実現は難しいところです。
例えば go-linq のようなライブラリもありますが、どうしてもいちいちinterfaceで受けざるを得ず型が崩壊したり、あるいはリフレクションを駆使していたりして、ちょっとコレジャナイ感があります。
そこで、こういうニーズに対しては go generate
を積極的に使って型やコードを生成していこうぜ、という方向性が一般的なようです。
gen
一部のプロジェクトでは、エンティティ系のstructに対して gen でslice操作用のメソッドを生やしています。
先ほどのコードの User
の上に以下のようなコメントを書き、
// +gen slice:"Where,Count,Select[AREA],Select[int],First,DistinctBy,Max[int],Min[int],SortBy"
type User struct {
ID int
Name string
Age int
Area AREA
}
該当ファイルと同じディレクトリで gen
を実行すると、
user_slice.go
user_slice.go
というファイルとtype UserSlice []User
が生成され、構造体の上に記述したメソッド群が扱えるようになります。
生成されたメソッドを使って先ほどの処理を書くとこんな感じになりました。
userList := Users{
User{ID: 1, Name: "Hoge", Age: 24, Area: AREA_TOKYO, Score: 200},
User{ID: 2, Name: "Fuga", Age: 14, Area: AREA_CHIBA, Score: 600},
User{ID: 3, Name: "Piyo", Age: 34, Area: AREA_TOKYO, Score: 300},
User{ID: 4, Name: "Foo", Age: 64, Area: AREA_KANAGAWA, Score: 450},
User{ID: 5, Name: "Bar", Age: 32, Area: AREA_KANAGAWA, Score: 200},
User{ID: 6, Name: "HogeX", Age: 20, Area: AREA_KANAGAWA, Score: 300},
User{ID: 7, Name: "FugaX", Age: 46, Area: AREA_CHIBA, Score: 700},
User{ID: 8, Name: "PiyoX", Age: 46, Area: AREA_KANAGAWA, Score: 400},
}
kanagawaHighScoreUserIDs := userList.ToUserSlice().
Where(func(user User) bool {
return user.Area == AREA_KANAGAWA && user.Score >= 300
}).
SortBy(func(user User, user2 User) bool {
return user.Age > user2.Age
}).
SelectInt(func(user User) int {
return user.ID
})
assert.Equal(t, 8, kanagawaHighScoreUserIDs[1])
ラムダ式のような記法が無い分、引数周りが若干冗長ですが、その辺のキモみに目を瞑るとまあなんとなく使えます。
First()
あたりも便利で、例えばTokyoの最高Scoreを持っているUserを取得するときはこうなります。
tokyoTopScoreUser, notFoundError := userList.ToUserSlice().
SortBy(func(user User, user2 User) bool {
return user.Score > user2.Score
}).
First(func(user User) bool {
return user.Area == AREA_TOKYO
})
assert.NoError(t, notFoundError)
assert.Equal(t, "Piyo", tokyoTopScoreUser.Name)
スコア1000超えのUserはいるか
isExistOverScore1000 := userList.ToUserSlice().
Count(func(user User) bool {
return user.Score > 1000
}) > 0
assert.False(t, isExistOverScore1000)
AREAごとのUserのmapを作る
var areaUserMap map[AREA]UserSlice = userList.
ToUserSlice().
GroupByAREA(func(user User) AREA {
return user.Area
})
Take()
あたりが無いのが残念ですが、こんな感じで gen、なかなかサクッと使えるケースが多いかと思います。