クロージャ完全に理解した...で何に使うの?
実は使いこなせると結構便利。特定の値に対する操作を共通化することが出来て、想定しているもの、言い方を変えれば作者の作成しているもの以外の操作を制限することも出来る。あと、かっこいい。
クロージャのユースケースとしては小さなデータベースに対してコマンド経由で何かをしたい時と考えると分かりやすい。今回はそのサンプルを4つ用意したので、便利さを体感してみてほしい。1つでも刺さるものがあれば嬉しい限り。
サンプル集
設計方針としては2つの考えをベースにしている。
- 初期値となる変数の用意(データ構造によってやりたいことを考える)
- 用意した初期値に対する操作(取得, 更新, 削除)を行うコマンドを引数で受け取る無名関数を作成してコマンドの判定を行い、それぞれに対応する処理を記述する
クロージャを使ったタイマー
動作確認はこちらから
play.golang.org
// closure timer -> 経過時間をsecondsで返す func ClosureTimer() func(c string) float64 { start := time.Now() return func(command string) float64 { switch command { case "GET": return time.Now().Sub(start).Seconds() case "STOP": return time.Now().Sub(start).Seconds() case "RESET": start = time.Now() return 0 } return 0 } }
実行結果
func main(){ // 新たなtimerを作成 newTimer := ClosureTimer() time.Sleep(1 * time.Second) // 経過時間が1sとなるはず fmt.Println(int(newTimer("STOP"))) newTimer("RESET") // timerがリセットされて0sとなるはず fmt.Println(int(newTimer("STOP"))) }
1 0
高階関数を使って関数内のローカルなスコープを持つ変数、この場合はClosureTimer
を呼び出した時の時刻を初期値として保持しておく。この時刻と現在時刻との差分を返すのが1番の目的(GET
or STOP
)となるが、その他に比較時間のリセットのためのRESET
というコマンドも用意してある。switch case
を増やせばいくらでも拡張できるため、都度都度、時刻時間の初期値を用意して差分を出して...値をリセットして...という変数管理をコマンドだけで行うことが出来る上にコードも共通化することが出来る。
自分は関数の実行時間を測る際にパッケージに記述したこの関数をよく使っていた。
クロージャを使ったカウンター
動作確認はこちらから
func ClosureCounter(init, inc int) func(c string) int { count := init return func(command string) int { switch command { case "ADD": count += inc return count case "RESET": count = init return count case "GET": return count } return -1 } }
実行結果
func main(){ // 新たなcounterを作成 counter := ClosureCounter(0, 1) // 初期値の確認 fmt.Println(counter("GET")) for i := 0; i < 10; i++ { counter("ADD") } // 正しくカウントされているかどうか fmt.Println(counter("GET")) // 値のリセット fmt.Println(counter("RESET")) }
0 10 0
やっていることは先ほど全く同じでデータ構造がint
に変わったのみ。ClosureCounter
を使うことでインクリメント変数の用意と管理を先ほどと同じように共通化、コマンド経由でのみ実行することが出来るため非常に便利。動作させているスレッドのカウントや、集計処理で条件にマッチしたデータ数をカウントするのによく使っていた。
クロージャを使ったkvs(キーバリューストア)
いわゆるredis
に近いことがスコープが有効な範囲でやれる。
動作確認はこちらから
play.golang.org
func ClosureKVS() func(c string, k string, v interface{}) interface{} { init := make(map[string]interface{}) return func(cmd string, k string, v interface{}) interface{} { switch cmd { case "ADD": init[k] = v return nil case "GET": res := init[k] return res case "RESET": init = make(map[string]interface{}) return nil case "KEYS": resp := make([]string, 0) for k, _ := range init { resp = append(resp, k) } return resp case "VALUES": resp := make([]interface{}, 0) for _, v := range init { resp = append(resp, v) } return resp } return nil } }
実行結果
func main(){ // 新たなkvsを作成 kvs := ClosureKVS() // key valueを追加 kvs("ADD", "okb", "cool") fmt.Println(kvs("GET", "okb", "")) // さらにkey valueを追加 kvs("ADD", "bko", "bad") fmt.Println(kvs("GET", "bko", "")) // 現在登録されているkeyを全取得 fmt.Println(kvs("KEYS", "", "")) // 現在登録されているvalueを全取得 fmt.Println(kvs("VALUES", "", "")) // 一度kvsをリセット fmt.Println(kvs("RESET", "", "")) // リセットが問題なく行われたかを確認 fmt.Println(kvs("KEYS", "", "")) }
cool
bad
[okb bko]
[bad cool]
<nil>
[]
正直、interface{}型
ってあんまり使うのは好きではないのですが、半ばしょうがなく採用。key
が常にstring型
なのは良いとしてvalue
の値の型を固定すると、都度都度、対応する型のkvs
を作る必要があるのでこんな形となった。
クロージャを使ったキュー
ここまで来たらもう何でも出来そう。やっぱりコマンド経由で...(ry
動作確認はこちらから
play.golang.org
func ClosureQueue() func(c string, v interface{}) interface{} { queue := make([]interface{}, 0) return func(cmd string, v interface{}) interface{} { switch cmd { case "QUEUE": queue = append(queue, v) return true case "DEQUEUE": if len(queue) > 0 { head := queue[0] queue = queue[1:len(queue)] return head } return nil case "SIZE": return len(queue) } return nil } }
実行結果
func main(){ // 新たなキューを作成 queue := ClosureQueue() queue("QUEUE", 1) queue("QUEUE", 2) queue("QUEUE", 3) fmt.Println(queue("SIZE", "")) fmt.Println(queue("DEQUEUE", "")) fmt.Println(queue("DEQUEUE", "")) fmt.Println(queue("DEQUEUE", "")) fmt.Println(queue("DEQUEUE", "")) queue("QUEUE", 99) fmt.Println(queue("DEQUEUE", "")) }
3 1 2 3 <nil> 99
最後に
どうでしょうか。クロージャも使ってみると意外とやれることが多いことに気づいてもらえれば何より。やはり値に対する操作を制限出来るというのが良い。想定外が発生しにくくなるし、共通化もされるため、インクリメント処理などを後から修正する事になっても、クロージャを使って共通化しておけば退屈な修正は最小限に済むだろう。