closure(クロージャー)とは何か
難しい概念の説明は強いエンジニアや大学の賢い先生方にお任せするため、深い説明は行わない。ざっくりと言うと、関数の中である値を保持させておいて、その値を変化させる(操作する)ための変数を保持している関数の内部に用意された無名関数のことだ。変数にアクセスするためにはclosure
経由でのみしか許可させない、変数の値を変化させるためにはclosure
経由でしか行わせないとすることで、変数のスコープを上手く隠すことが可能になる。実際にgolang
で書いたclosure
と動作をお見せする
golangで作ったclosureを利用したカウンター
実装はめちゃくちゃシンプル。go tour
で解説されているclosure
のサンプルを引数のコマンド経由で色んな処理が行える様にしただけだ。なぜ、このカウンターを作成したかというと、業務でgolang
を使って、json
からマーシャルしたデータを集計して返すみたいな処理が多発しており、大量のカウント用の変数の用意と管理がめんどくさくて、何かオシャレにしたろと思い出来上がったのがこれ。コマンドで操作できるので、割と気に入ってるし、コードの改修も行いやすそうだと思っている
package main import "fmt" // カウンターをclosureで作成。コマンドによって処理を切り分ける func ClosureCounter(init, increment int) func(string) int { sum := init return func(command string) int { // commandを用いて行いたい処理を切り替える switch command { // 引数で指定した値分、sumに加算する case "ADD": sum += increment return sum // sumの値を返す case "GET": return sum // sumの値をリセットする case "RESET": sum = 0 return sum } return sum } } // 実行部分 func main() { counter := ClosureCounter(0, 1) for i := 0; i < 10; i++ { counter("ADD") fmt.Println(counter("GET")) } fmt.Println("最終結果: ", counter("GET")) // 値をリセット counter("RESET") fmt.Println("RESET後: ", counter("GET")) }
結果
1 2 3 4 5 6 7 8 9 10 最終結果: 10 RESET後: 0
このように動作する。先ほどの説明の通り、sumという変数の操作には、return
で返された無名関数とコマンド(引数)経由でのみしか行うことが出来ない
では本命のElixirで
無名関数と聞いてElixirで実装しないわけにはいかない。さくっと作れそうなので作ってみた。と、思ったがこれがかなり難しい。関数型言語で値が変化されないことが約されるので、先ほどのように元の変数に対して操作を行うという処理をするのは想像以上に難しい。というか出来ない。なので、あれこれ工夫した結果、以下の様な形に追いついた。一言で言えば、プロセスを立ち上げてメッセージを送る方法になる
./counter.ex
defmodule LikeClosure do # 受け取ったメッセージを元に処理を分岐 def loop(sum, increment) do receive do # 引数で受け渡し分、加算を行う {:add, pid} -> send(pid, {:ok, sum+increment}) loop(sum+increment, increment) # 値をリセット {:reset, pid} -> send(pid, {:ok, 0}) loop(0, increment) # 現在の値を返す {:get, pid} -> send(pid, {:ok, sum}) loop(sum, increment) # プロセスを安全に停止させる {:exit, pid} -> send(pid, {:exit}) exit(:safety) end end # コマンドの送信と受け取り def messenger(pid) do # カリー化した無名関数を返す fn command -> send(pid, {command, self()}) receive do # 返答を受け取る {:ok, sum} -> sum {:exit} -> IO.puts("stop counter and counter process is exit") _ -> IO.pus(":error") end end end # 処理を内部化して使用者に意識させない def counter(sum, increment) do counter_pid = spawn(__MODULE__, :loop, [sum, increment]) messenger(counter_pid) end end
流れとしては、カウント用のプロセスを立ち上げて、そのプロセスに対してメッセージという形でコマンドを送り、プロセスが保持している値(カウンター)を操作するという形になる。また、その一連の処理を簡潔に行いたいので、counter
という関数にプロセスの生成から、メッセージのパッシングまでを内部化させて使用者に見えない様にしている。動作を見てみよう
# ファイルをコンパイル iex(1) > c("counter.ex") [LikeClosure] # 無名関数をreturnで受け取る iex(3)> counter = Closure.counter(0,1) #Function<0.133727158/1 in Closure.messenger/1> # コマンドを送って指定の操作をさせる iex(4)> counter.(:add) 1 iex(5)> counter.(:add) 2 iex(6)> counter.(:add) 3 : : iex(16)> counter.(:add) 13 iex(17)> counter.(:add) 14 # 値の確認 iex(18)> counter.(:get) 14 # 値のリセット iex(19)> counter.(:reset) 0 iex(20)> counter.(:get) 0 # プロセスの停止 iex(21)> counter.(:exit) stop counter and counter process is exit :ok
思ったことが出来ている。使用者には内部の実装を意識させないようにすることも出来た。しかしながら、カウントさせるだけの処理にこれだけコード量を記述するのは正直なところ「うーん」という感じなので、何か良い方法はないかと模索していたところ、以下の回答を発見した
なるほど、Agent
を使えば、確かに簡単に実装できそう。なので次はAgent
で同様の処理をさせてみた
iex(6)> {:ok, pid} = Agent.start_link(fn -> 0 end)
{:ok, #PID<0.113.0>}
iex(7)> fun = fn -> Agent.get_and_update(pid, fn i -> {i, i + 1} end) end
_#Function<20.127694169/0 in :erl_eval.expr/5>
iex(8)> fun.()
0
iex(9)> fun.()
1
iex(10)> fun.()
2
iex(11)> fun.()
3
iex(12)> fun.()
4
かなり、短くなった上に実装がシンプルに出来た
./counter.ex
defmodule AgentClosure do use Agent def start_link(init, increment) do # Agentプロセスを立ち上げ Agent.start_link(fn -> init end, name: __MODULE__) IO.puts("start counter agent") # パターンマッチを使用するためと内部化のため、無名関数を返す fn command -> counter(command, increment) end end # 加算 def counter(:add, increment) do Agent.update(__MODULE__, &(&1 + increment)) end # 値の確認 def counter(:get, _) do Agent.get(__MODULE__, & &1) end # 値のリセット def counter(:reset, _) do Agent.update(__MODULE__, fn _ -> 0 end) end end
結果
iex(1)> c("counter.ex") [AgentClosure, LikeClosure] iex(2)> counter = AgentClosure.start_link(0, 1) start counter agent #Function<4.81008950/1 in AgentClosure.start_link/2> iex(3)> counter.(:add) :ok iex(4)> counter.(:add) :ok iex(5)> counter.(:add) :ok iex(6)> counter.(:get) 3 iex(7)> counter.(:reset) :ok iex(8)> counter.(:get) 0
同様の使用感で使うことが出来る。というか、これ関数のみで外部からの変化を許可していないので普通にclosure
になっていることに今更気づいた
おまけ
上手くいかなかった時のコードその1
失敗: golang
で書いた感じでそのまま移植しようとする
defmodule Closure do def counter() do sum = 0 fn command -> case command do "ADD" -> sum = sum + 1 IO.puts(sum) sum "RESET" -> sum = 0 sum "GET" -> sum end end end end counter = Closure.counter() IO.puts(counter.("GET")) Enum.map(1..10, fn _ -> counter.("ADD") end) IO.puts(counter.("GET"))
結果
0 1 1 1 1 1 1 1 1 1 1 0
そりゃ、そうなる
上手くいかなかった時のコードその2
失敗: 関数型で値を保持させるなら再帰関数使ってアキュムレーターやんけと思ってカリー化して値の保持までは出来たものの、結局、sumの値は不変なので「無理ゲーでは?」と気づく
defmodule Closure do def counter(init, increment) do sum = init fn command -> closure(command, sum, increment) end end def closure("ADD", sum, increment) do sum + increment # 結局sumに値がreturnされないので値を保持できない... # プロセス用意してmessage passingさせれば無理やりカウント出来るが... end def closure("GET", sum, _) do sum end def closure("RESET", _, _) do 0 end end # 変数のBINDまでは出来た counter = Closure.counter(0, 2) IO.inspect(counter) counter.("GET") |> IO.puts() counter.("ADD") |> IO.puts() counter.("ADD") |> IO.puts()
結果
#Function<0.30015443/1 in Closure.counter/2> 0 2 2
惜しいけど、カリー化という知見が身についた