【Golang】コアラのように怠惰でチンパンジーのように賢い、高速配列操作ライブラリ「Koazee」使ってみた

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419037/8aff6bde-27c2-e5c5-d364-781e3fbbd0a7.png)

今回はGolangの配列操作ライブラリ「Koazee」について紹介したいと思います。
「そもそもKoazeeとは？お前C#一筋ちゃうんか」すみません。

公式ドキュメントによると、
`"Lazy like a Koala, smart like a chimpanzee" (コアラのように怠惰でチンパンジーのように賢い)` 
ライブラリです。Koa(ra) (chimpan)zee という意味みたいですね。

このライブラリ、とてつもなく**速い**らしいです。
作者による[Medium.comの投稿](https://medium.com/wesovilabs/koazee-vs-go-funk-vs-go-linq-caf8ef18584e)からベンチマークを引用すると、
他のGoの配列操作ライブラリと比べて以下の画像の通りの速度差があるとのこと。マユツバ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419037/2e47e10a-8109-9dab-8f63-7ab1cbb97f80.png)

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するまで](https://qiita.com/nooboolean/items/11805928527aeb576c21)を参考にさせて頂き、環境構築を進めていきます。
「Goもう入ってるよ！」という方は [2番](https://qiita.com/nqdior/items/e225eae820d6157dc05b#2-koazee%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB) へGOGO！！

### 環境
* CentOS7:latest

### Golang インストール

epelリポジトリをインストールし、golangをインストールする。

```console
# yum install epel-release
# yum install -y golang
```

テンプレートディレクトリを作成する。

```
# mkdir /usr/local/go
# cd /usr/local/go/
# mkdir bin pkg src
```

インストール完了後、以下のコマンドでバージョンを表示し、インストールされていることを確認します。

```console
# go version
go version go1.11.5 linux/amd64
```

### パスの設定

パスを通していきます。bash_profileを以下の通り編集。

```console
# vi ~/.bash_profile
```

```bash:~/.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` それぞれ正常に出力されるか確認します。

```console
# 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/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419037/b2555972-02b5-e110-6a25-d2e53561b251.png)

### Golang インストール

ダウンロード後、起動してナビゲーションの通り進めていくとインストールが完了します。
デフォルトだとCドライブ直下にインストールされるようなので、Cを汚染されたくない場合は変更していいと思われ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419037/3456eda6-d75b-f77e-d9f7-9052cda1fecc.png)

インストール完了後、コンソールでバージョンを表示し、インストールされていることを確認します。
インストーラでパスの設定まで行われているようですので、別途設定する必要はありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/419037/f47f93ad-0f45-53ec-35a5-3e25e466aa22.png)

これでWindowsへのGoのインストールは完了！
では引き続きKoazeeのインストールと使用に進んでいきます。

## 2. Koazeeのインストール

`https://github.com/wesovilabs/koazee`からライブラリを取得します。
出力は特に吐かれません。ワイだけ？

```console
# go get github.com/wesovilabs/koazee
```

使用する際の参照方法はこんな感じ。

```go:sample.go
package main

import (
	"github.com/wesovilabs/koazee"
)
```

早速公式のサンプルコードを実行してみましょう！

## 3. Koazeeのサンプルプログラム

以下、公式ドキュメントより転載したコードにコメントをつけています。

### 実行サンプル1 - 指定した要素を取り出す

```go:02_at_first_last.go 
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 - 要素の追加・削除

要素の追加・削除も以下の通りおちゃのこさいさい。

```go:03_add_delete.go 
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 - 要素の抽出

```go:06_take_filter_duplicate.go 
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 - 要素の集計

```go:07_groupby.go  
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](https://github.com/wesovilabs/koazee/wiki)に詳細な解説があるので[公式ドキュメント](https://github.com/wesovilabs/koazee)と合わせてご活用ください。

### 実行サンプル5 - 遅延実行（だいじ）

さて、最後にkoazeeで自分が一番注目した **遅延実行** について紹介します。
以下のコードをご覧ください。

```go:11_lazyEvaluate.go
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オブジェクトに格納** します。
（厳密には式を格納という表現もおかしいですが、ひとまずはその理解で大丈夫です。）
みなさんよくご存じの即時実行関数ではないので、ここでは出力は行われません。

```go
	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() が実行されたタイミングで初めて式が評価され、値が確定します。

```go
    // 実行するまで式は評価されないぜ。
	fmt.Println("Operations are not evaluated until we perform stream.Do()\n")

    // 実行と同時に評価される。ここで結果がコンソール出力される！
	stream.Do()
```

遅延実行の何が嬉しいかというと、**遅延により計算の全体像をとらえた上で演算処理が実行されるため、余計な計算を行わずに済む** という点です。
C#erならばLinq、Scalaer(Scara+er?)ならばStreamの概念と同じですね。
遅延評価についてはまた別記事を作成したいと思いますので、「は？」という方はもうしばらくお待ちくださいませ。。。

## 4. まとめ

いかがでしょうか？
本稿で使用した資料を以下の通りまとめておきましたので、ぜひご参照ください。

- [**GitHub**](https://github.com/wesovilabs/koazee) ... Koazee公式GitHub。今回使用したサンプルはすべて載っている。
- [**Wiki**](https://github.com/wesovilabs/koazee/wiki) ... Koazee公式Wiki
- [**Medium**](https://medium.com/wesovilabs/koazee-vs-go-funk-vs-go-linq-caf8ef18584e) ... 作者による他配列操作ライブラリとのベンチマーク。

このライブラリを使用すれば既存のGolangコードに対して、更なる高速化アプローチが可能になりますね！
ご質問等あればお気軽にコメントくださいませ～。

## 5. 追記：サンプルコード

簡単な時間計測用サンプルコードを作ってみました。
自由に改変していろいろ触ってみてください。

```go:koazeeFilter.go
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サイコー！！