【サンプルコード有り】golangとclosureで作ったクールなカウンターをElixirで書き直した

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

思ったことが出来ている。使用者には内部の実装を意識させないようにすることも出来た。しかしながら、カウントさせるだけの処理にこれだけコード量を記述するのは正直なところ「うーん」という感じなので、何か良い方法はないかと模索していたところ、以下の回答を発見した

elixirforum.com

なるほど、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

惜しいけど、カリー化という知見が身についた

参考文献