はじめに
Enum.reduce/3関数について、初期値と累積値の関係を改めて確認した結果を整理しました。今までちょっとあやふやな理解でしたが、Enum.reduce/3関数に対してしっかり理解できました。
書式
関数部を書くときはfn x, acc -> func(x, acc)のように各値:x と累積値:accの2つの引数を必ずとります。
結論
result = Enum.reduce([x1, x2, x3..., xn], init, fn x, acc -> func(x, acc) end)
と書く時は以下が実行される。
ans1 = func(x1, init) # init: 指定したaccの初期値
ans2 = func(x2, ans1)
ans3 = func(x3, ans2)
:
result = func(xn, ans(n-1))
ElixirのEnum.reduceを一発で理解する方法の記事では以下のように記載されていました。
reduce [1,2,3,4..n] init fun = (fun n ... (fun 4 (fun 3 (fun 2 (fun 1 init)))) ...)
この書き方は最初は飲み込めませんでしたが、
acc = init
acc = fun 1 init
acc = fun 2 (fun 1 init)
:
と伝搬されていくことがわかれば、理解しやすいと思います。
一方、Elixir ~Enum.reduceを分解、理解しよう~の記事では以下のように記載されていました。
init
|> M.func(1)
|> M.func(2)
|> M.func(3)
こちらの書き方はaccumulatorの伝搬という意味では理解しやすいと思いますが、accの値の変化という意味ではちょっとわかりづらいと感じました。
ケース1
簡単なケース
iex> Enum.reduce(1..3, "start", fn x, acc -> "#{acc} plus #{x}" end)
"start plus 1 plus 2 plus 3"
展開
iex> "#{acc} plus #{x}" <-- acc: "start"(指定した初期値), x: 1
"start plus 1"
iex> "#{acc} plus #{x}" <-- acc: "start plus 1"(前回出力値) , x: 2
"start plus 1 plus 2"
iex> "#{acc} plus #{x}" <-- acc: "start plus 1 plus 2"(前回出力値) , x: 3
"start plus 1 plus 2 plus 3"
ちなみにEnum.reduce/2だと...
iex> Enum.reduce(1..3, fn x, acc -> "#{acc} plus #{x}" end)
"1 plus 2 plus 3"
1回目の関数は実行されず、acc = x1 が実行されるだけみたい。(hexdoc.pmにかかれてますね)
ケース2
書籍プログラミングElixirのプロセスのオーバーヘッドでのChain.create_processes関数
defmodule Chain do
def counter(pid) do
receive do
n -> send pid, n + 1
end
end
def create_processes(n) do
last = Enum.reduce (1..n, self(),
fn _, send_to ->
spawn(Chain, :counter, [send_to])
end)
send (last, 0)
end
end
展開
last = Enum.reduce(1..n, self(),
fn _, send_to ->
spawn(Chain, :counter, [sent_to])
end)
は以下のような展開になります。
iex> pid1 = spawn(Chain, :counter, [self()]) # acc:self() 指定した初期値, x: 不使用
# PID<0.140.0> (1例です)
iex> pid2 = spawn(Chain, :counter, [pid1]) # acc:pid1, x: 不使用
# PID<0.142.0> (1例です)
iex> pid3 = spawn(Chain, :counter, [pid2]) # acc:pid2, x: 不使用
# PID<0.144.0> (1例です)
:
last = spawn(Chain, :coutner, [pid(n-1)]) # acc:pid(n-1), x: 不使用
# PID<0.150.0> (1例です)
- enumerableを(1..n)とすることによって、n回の関数実行(プロセス生成)とする。
- enumerableの各値は関数内では使用していない。
- 各回の返り値(pid)をそのまま次の関数の引数とすることによって、pidに関連付けて連鎖的にプロセスを生成する。
ということになります。
Enum.reduce/3から脱線しますが、send (last, 0)にてメッセージを送ると、先ほどプロセスを生成した順序とは逆の方向にメッセージが連鎖的に伝搬されていきます。

ケース3
mapの更新。
Elixirでナンプレを解くコードを書いてみたの記事にてmapの更新を以下のように書きました。
Enum.reduce(getkey(board, n), option, fn x, acc -> Map.update!(acc, x, fn value -> value -- [intg] end) end)
ここでoptionは以下のようなmapです。
option = %{
"00" => [2, 3]
"01" => [2, 6]
"02" => [1, 7, 9]
"03" => [2, 6, 7]
:
"88" => [5, 7]
} #1例です。
getkey関数の返り値は、値の更新対象となるキーのリストです。
getkey = ["10", "11", "12", "13", ..."79"] #1例です。
展開
Enum.reduce(["10", "11", "12", "13"], option, fn x, acc -> Map.update!(acc, x, fn value -> value -- [3] end) end)
は以下のような展開となります。
iex> option1 = Map.update!(option , "10", Map.get(option , "10") -- [3])
%{"00" => []...}
iex> option2 = Map.update!(option1, "11", Map.get(option1, "11") -- [3])
%{"00" => []...}
iex> option3 = Map.update!(option2, "12", Map.get(option2, "12") -- [3])
%{"00" => []...}
iex> result = Map.update!(option3, "13", Map.get(option1, "13") -- [3])
%{"00" => []...}
mapにて更新するキーのみの値をまとめて変更し、その他のキーの値を引き継ぐ書き方ができました。