今回はGolangの配列操作ライブラリ「Koazee」について紹介したいと思います。
「そもそもKoazeeとは?お前C#一筋ちゃうんか」すみません。
公式ドキュメントによると、
"Lazy like a Koala, smart like a chimpanzee" (コアラのように怠惰でチンパンジーのように賢い)
ライブラリです。Koa(ra) (chimpan)zee という意味みたいですね。
このライブラリ、とてつもなく速いらしいです。
作者によるMedium.comの投稿からベンチマークを引用すると、
他のGoの配列操作ライブラリと比べて以下の画像の通りの速度差があるとのこと。マユツバ。
Koazeeの特徴は以下の通り。
- Immutable: Koazee won't modify your inputs.
- StreamLike: We can combine operations up our convenience.
- Lazy loading: Operations are not performed until they're required
- Generic: Koazee provides a generic interface able to deal with slice of any type without creating custom functions.
- Focusing on performance: First rule for implementing a new operation is providing the best possible performance.
個人的に惹かれたのは Immutable と Lazy loading。ほら、C#erならわかるでしょ(?)。
ではさっそくこのKoazeeを使っていきたいと思います。
1. Golang 環境構築(Linux編)
CentOS 7にyumでGoを入れてHello Worldするまでを参考にさせて頂き、環境構築を進めていきます。
「Goもう入ってるよ!」という方は 2番 へGOGO!!
環境
- CentOS7:latest
Golang インストール
epelリポジトリをインストールし、golangをインストールする。
# yum install epel-release
# yum install -y golang
テンプレートディレクトリを作成する。
# mkdir /usr/local/go
# cd /usr/local/go/
# mkdir bin pkg src
インストール完了後、以下のコマンドでバージョンを表示し、インストールされていることを確認します。
# go version
go version go1.11.5 linux/amd64
パスの設定
パスを通していきます。bash_profileを以下の通り編集。
# vi ~/.bash_profile
# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# User specific environment and startup programs
GOPATH=/usr/local/go
PATH=$GOPATH/bin:$PATH:$HOME/bin
export PATH
export GOPATH
変更したファイルを適用し、GOPATH
, PATH
それぞれ正常に出力されるか確認します。
# source ~/.bash_profile
# echo $GOPATH
/usr/local/go
# echo $PATH
/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
確認ができたらインストールは完了!
1. Golang 環境構築(Windows編)
では続いてGolangのインストールを行います。
「えっ!? 今Linuxにインストールしたよね!? 痴呆か?」 と思われたアナタ。
大丈夫です。まだボケてはいません。
今回作業中に帰省してしまったので、手元のWindows10にも環境構築しました。
せっかくなのでWindowsのインストール方法を以下の通りまとめておきます。
Linuxで開発を行われる方はこの手順はもちろん不要です。
環境
- Windows10 Home
インストーラのダウンロード
公式ページにアクセスし、以下の赤枠のリンクからインストーラをダウンロードする。
https://golang.org/dl/
Golang インストール
ダウンロード後、起動してナビゲーションの通り進めていくとインストールが完了します。
デフォルトだとCドライブ直下にインストールされるようなので、Cを汚染されたくない場合は変更していいと思われ。
インストール完了後、コンソールでバージョンを表示し、インストールされていることを確認します。
インストーラでパスの設定まで行われているようですので、別途設定する必要はありません。
これでWindowsへのGoのインストールは完了!
では引き続きKoazeeのインストールと使用に進んでいきます。
2. Koazeeのインストール
https://github.com/wesovilabs/koazee
からライブラリを取得します。
出力は特に吐かれません。ワイだけ?
# go get github.com/wesovilabs/koazee
使用する際の参照方法はこんな感じ。
package main
import (
"github.com/wesovilabs/koazee"
)
早速公式のサンプルコードを実行してみましょう!
3. Koazeeのサンプルプログラム
以下、公式ドキュメントより転載したコードにコメントをつけています。
実行サンプル1 - 指定した要素を取り出す
package main
import (
"fmt"
"github.com/wesovilabs/koazee"
)
var numbers = []int{1, 5, 4, 3, 2, 7, 1, 8, 2, 3}
func main() {
// koazee.StreamOfメソッドを使用して配列をKoazee形式で格納する。
stream := koazee.StreamOf(numbers)
// koazee形式で配列が格納されたstreamオブジェクトのメソッドをコールし、値を出力する。
// 指定されたインデックスの要素を取得
fmt.Printf("stream.At(4): %d\n", stream.At(4).Int())
// 最初の要素を取得
fmt.Printf("stream.First: %d\n", stream.First().Int())
// 最後の要素を取得
fmt.Printf("stream.Last: %d\n", stream.Last().Int())
}
/** output
go run main.go
stream.At(4): 2
stream.First: 1
stream.Last: 3
*/
わかりやすいですね!
実行サンプル2 - 要素の追加・削除
要素の追加・削除も以下の通りおちゃのこさいさい。
package main
import (
"fmt"
"github.com/wesovilabs/koazee"
)
var numbers = []int{1, 5, 4, 3, 2, 7, 1, 8, 2, 3}
func main() {
fmt.Printf("input: %v\n", numbers)
stream := koazee.StreamOf(numbers)
// 要素の追加
fmt.Print("stream.Add(10): ")
fmt.Println(stream.Add(10).Do().Out().Val())
// 要素の削除
fmt.Print("stream.Drop(5): ")
fmt.Println(stream.Drop(5).Do().Out().Val())
// 範囲を指定して要素の削除
fmt.Print("stream.DropWhile(val<=5): ")
fmt.Println(stream.DropWhile(func(element int)bool{return element<=5}).Do().Out().Val())
// インデックスを指定して要素の削除
fmt.Print("stream.DeleteAt(4): ")
fmt.Println(stream.DeleteAt(4).Do().Out().Val())
// インデックスを指定して要素の値の変更(0番目のインデックスの要素を5に書き換え)
fmt.Print("stream.Set(0,5): ")
fmt.Println(stream.Set(0, 5).Do().Out().Val())
// 最初の要素を抽出し、自身と新しいストリームを生成する。(output参照)
fmt.Print("stream.Pop(): ")
val, newStream := stream.Pop()
fmt.Printf("%d ... ", val.Int())
fmt.Println(newStream.Out().Val())
}
/** output
go run main.go
input: [1 5 4 3 2 7 1 8 2 3]
stream.Add(10): [1 5 4 3 2 7 1 8 2 3 10]
stream.Drop(5): [1 4 3 2 7 1 8 2 3]
stream.DropWhile(val<=5): [7 8]
stream.DeleteAt(4): [1 5 4 3 7 1 8 2 3]
stream.Set(0,5): [5 5 4 3 2 7 1 8 2 3]
stream.Pop(): 1 ... [5 4 3 2 7 1 8 2 3]
*/
実行サンプル3 - 要素の抽出
package main
import (
"fmt"
"github.com/wesovilabs/koazee"
)
var animals = []string{"lynx", "dog", "cat", "monkey", "dog", "fox", "tiger", "lion"}
func main() {
fmt.Print("input: ")
fmt.Println(animals)
stream := koazee.StreamOf(animals)
// 範囲を指定して要素を抽出
fmt.Print("stream.Take(1,4): ")
fmt.Println(stream.Take(1, 4).Out().Val())
// 条件を指定して要素を抽出
fmt.Print("stream.Filter(len==4): ")
fmt.Println(stream.
Filter(
func(val string) bool {
return len(val) == 4
}).
Out().Val(),
)
// 重複する要素を省く
fmt.Print("stream.RemoveDuplicates(): ")
fmt.Println(stream.RemoveDuplicates().Out().Val())
}
/**
go run main.go
input: [lynx dog cat monkey dog fox tiger lion]
stream.Take(1,4): [dog cat monkey dog]
stream.Filter(len==4): [lynx lion]
stream.RemoveDuplicates(): [lynx dog cat monkey fox tiger lion]
*/
実行サンプル4 - 要素の集計
package main
import (
"fmt"
"github.com/wesovilabs/koazee"
"strings"
)
var animals = []string{"lynx", "dog", "cat", "monkey", "dog", "fox", "tiger", "lion"}
func main() {
fmt.Printf("input: %v\n", animals)
stream := koazee.StreamOf(animals)
fmt.Print("stream.GroupBy(strings.Len): ")
// 文字列の長さでグルーピングする
out, _ := stream.GroupBy(func(val string)int{return len(val)})
fmt.Println(out)
}
/**
go run main.go
input: [lynx dog cat monkey dog fox tiger lion]
// 文字列の長さごとにグルーピングされて出力される
stream.GroupBy(strings.Len): map[5:[tiger] 4:[lynx lion] 3:[dog cat dog fox] 6:[monkey]]
*/
そのほかのメソッドについてはWikiに詳細な解説があるので公式ドキュメントと合わせてご活用ください。
実行サンプル5 - 遅延実行(だいじ)
さて、最後にkoazeeで自分が一番注目した 遅延実行 について紹介します。
以下のコードをご覧ください。
package main
import (
"fmt"
"github.com/wesovilabs/koazee"
"strings"
)
type Person struct {
Name string
Male bool
Age int
}
var people = []*Person{
{"John Smith", true, 32},
{"Peter Pan", true, 17},
{"Jane Doe", false, 20},
{"Anna Wallace", false, 35},
{"Tim O'Brian", true, 13},
{"Celia Hills", false, 15},
}
func main() {
// 渡された配列から女性だけを抽出してソートしてForeachで出力する処理を格納する
stream := koazee.
StreamOf(people).
Filter(func(person *Person) bool {
return !person.Male
}).
Sort(func(person, otherPerson *Person) int {
return strings.Compare(person.Name, otherPerson.Name)
}).
ForEach(func(person *Person) {
fmt.Printf("%s is %d years old\n", person.Name, person.Age)
})
// 実行するまで式は評価されないぜ。
fmt.Println("Operations are not evaluated until we perform stream.Do()\n")
// 実行と同時に評価される。
stream.Do()
}
/** output
go run main.go
// 実行するまで式は評価されてないぜ。
Operations are not evaluated until we perform stream.Do()
// stream.Do() の実行時に格納された式が評価され、出力される。
Anna Wallace is 35 years old
Celia Hills is 15 years old
Jane Doe is 20 years old
*/
いかがでしょうか?(NAVER風(前回やった))
拙いコメントでの説明となってしまったので、順を追って説明します。
まずは以下の個所で、式をstreamオブジェクトに格納 します。
(厳密には式を格納という表現もおかしいですが、ひとまずはその理解で大丈夫です。)
みなさんよくご存じの即時実行関数ではないので、ここでは出力は行われません。
stream := koazee.
StreamOf(people).
Filter(func(person *Person) bool {
return !person.Male
}).
Sort(func(person, otherPerson *Person) int {
return strings.Compare(person.Name, otherPerson.Name)
}).
ForEach(func(person *Person) {
fmt.Printf("%s is %d years old\n", person.Name, person.Age)
})
式の格納後、次の行で出力されたメッセージ Operations are ~
がコンソール出力されます。
そして stream.Do() が実行されたタイミングで初めて式が評価され、値が確定します。
// 実行するまで式は評価されないぜ。
fmt.Println("Operations are not evaluated until we perform stream.Do()\n")
// 実行と同時に評価される。ここで結果がコンソール出力される!
stream.Do()
遅延実行の何が嬉しいかというと、遅延により計算の全体像をとらえた上で演算処理が実行されるため、余計な計算を行わずに済む という点です。
C#erならばLinq、Scalaer(Scara+er?)ならばStreamの概念と同じですね。
遅延評価についてはまた別記事を作成したいと思いますので、「は?」という方はもうしばらくお待ちくださいませ。。。
4. まとめ
いかがでしょうか?
本稿で使用した資料を以下の通りまとめておきましたので、ぜひご参照ください。
- GitHub ... Koazee公式GitHub。今回使用したサンプルはすべて載っている。
- Wiki ... Koazee公式Wiki
- Medium ... 作者による他配列操作ライブラリとのベンチマーク。
このライブラリを使用すれば既存のGolangコードに対して、更なる高速化アプローチが可能になりますね!
ご質問等あればお気軽にコメントくださいませ~。
5. 追記:サンプルコード
簡単な時間計測用サンプルコードを作ってみました。
自由に改変していろいろ触ってみてください。
package main
import (
"fmt"
"math/rand"
"time"
"github.com/wesovilabs/koazee"
)
var numbers = []int{}
func main() {
maxValue := 100
dummyCount := 5 * 10000000
fmt.Printf("1. 0~100までのランダムな数値%d件を生成。\n", dummyCount)
rand.Seed(time.Now().UnixNano())
for i := 0; i < dummyCount; i++ {
numbers = append(numbers, rand.Intn(maxValue))
}
fmt.Printf("2. %d件生成完了。Koazeeに読み込み。\n", dummyCount)
stream := koazee.StreamOf(numbers)
/*
このコメントアウトを外したら一覧が出力できます。超重いよ。
fmt.Print("Koazeeに読み込み完了。一覧はコチラ ↓ \n")
fmt.Println(stream.Out().Val())
fmt.Println("\n")
*/
start := time.Now() // filter start
// 値が10の要素だけを抽出してカウント
count, _ := stream.Filter(func(value int) bool {
return value == 10
}).Count()
end := time.Now() // filter end
fmt.Printf("値が10の要素だけを%f秒で抽出しました。\n", (end.Sub(start)).Seconds())
fmt.Printf("対象件数は %d 件です。\n", count)
}
/**
go run koazeeFilter.go
1. 0~100までのランダムな数値50000000件を生成。
2. 50000000件生成完了。Koazeeに読み込み。
値が10のものだけを0.179520秒で抽出しました。
対象件数は 499951 件です。
*/
PCスペックにもよると思いますが、0.2秒かからないですね。Koazeeサイコー!!