#ここまでのあらまし
そろそろ最初にJavaに触れてプログラミング学習をはじめてから半年。
直後にElixirを志すことにしてドキュメントの写経や動作確認をするようになってからやはり半年弱。
北九州に移り住んで、音楽やデザインと並行しながら直接メンターの先生にElixirを教わるようになって2ヶ月ほど。
Elixirはじめプログラミングの学習効率が全く思うように上がらない、定着しない壁を感じていました。
そこで、これまでに書いた自分のドキュメントを振り返り、理解および理解できないことの認識を新たにすることにしました。
そして、ひょんなことからメンターの山崎進先生 が、わたしとの学習に際してのやりとりをTwitter上で連続ツイートの形で公開されました。
今回は、それに即して今回わたしの疑問にあがったEnum.reduceの式を分解、理解していこうと思います!
#本題
今回あつかう式は以下です。
iex> Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)
— Susumu Yamazaki (@zacky1972) 2019年5月3日
6
iex> Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)
6
最初、わたしの理解は第一引数のリストの総和を求める式、というものでした。
じゃあ第二引数のゼロは?第三引数はxとaccの和に見えるのになぜこれが総和を求める式になるのか?accとは?という感じでした。
もうわかりましたね?
— Susumu Yamazaki (@zacky1972) 2019年5月3日
iex> Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)
は
1+2+3 = 6
を計算しているようです。
しかし、第一引数をいじるかぎりわたしのここまでの仮説、つまり第一引数のリストの数値の総和、という仮説は合っているように思えます。
続いて第二引数。
では次に第2引数を変更して推測してみましょう。
— Susumu Yamazaki (@zacky1972) 2019年5月3日
iex> Enum.reduce([1, 2, 3], 1, fn x, acc -> x + acc end)
これは?
iex> Enum.reduce([1, 2, 3], 2, fn x, acc -> x + acc end)
こうだと?
iex> Enum.reduce([1, 2, 3], -1, fn x, acc -> x + acc end)
これならどうでしょう?
それぞれ、
iex> Enum.reduce([1,2,3],1, fn x,acc -> x + acc end)
7
iex> Enum.reduce([1,2,3],2, fn x,acc -> x + acc end)
8
iex> Enum.reduce([1,2,3],-1, fn x,acc -> x + acc end)
5
わたしは第一引数の総和に対して加算減算しているのではないか、と思いました。
しかしこれはEnum.reduceの本来の動きからすると正確ではなく、
わかりました?
— Susumu Yamazaki (@zacky1972) 2019年5月3日
実はさっき
iex> Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)
は
1+2+3 = 6
と言ったのですが,
より正確には
0 (第2引数) + 1 + 2 + 3 = 6
だったんです!
というのが正解でした。
関門である第三引数に移ります。
iex> Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)
— Susumu Yamazaki (@zacky1972) 2019年5月3日
第3引数が無名関数を使っているので,初学者には理解しづらいかもしれません。次のように名前のある関数に直しておきますね。
defmodule M do
def func(x, acc) do
x + acc
end
end
Enum.reduce([1, 2, 3], 0, & M.func(&1, &2))
Enum.reduce([1,2,3],0, fn x,acc -> x + acc end)
を
defmodule M do
def func(x,acc) do
x + acc
end
end
Enum.reduce([1,2,3],0, & M.func(&1,&2))
と書き換えます。
実は Enum.reduce/3 は本当は次のような計算をしています。
— Susumu Yamazaki (@zacky1972) 2019年5月3日
M.func(3, M.func(2, M.func(1, 0)))
パイプライン演算子を使うと次の通りです。
0
|> M.func(1)
|> M.func(2)
|> M.func(3)
M.func(3, M.func(2, M.func(1, 0)))
を、外から読もうとしてうまく掴めなかったのですが、(1,0)の計算結果が次の第二引数になって〜という流れで、
0
|> M.func(1)
|> M.func(2)
|> M.func(3)
の構造を理解することができました。
つまり Enum.reduce/3 は
— Susumu Yamazaki (@zacky1972) 2019年5月3日
第2引数を初期値として,パイプライン演算子で数珠つなぎに次々と,第1引数のリストの各要素を順番に取り出して第3引数の関数を適用し,第1リストの最終要素まで適用した累積結果を返す
ということなんです。
つまり、
総和を求めるのではなく、累積して要素に関数を適用する、Enum.reduceの第二引数、つまり起点がゼロで、第三引数の式がxとacc(それまでの累積)の和だから、結果としてこの式は総和を求めているように見える式になる。
— Kento Mizuno (@kmizuno0211) 2019年5月3日
ゼロを起点に、リストの各要素とそれまでの計算結果を累積して足していく。
自分の理解と日本語でのアウトプットをうまく繋げるのに骨が折れるのですが、こんなところでしょうか。
「累積」という言葉が出ましたが,累積を英訳すると,accumulate
— Susumu Yamazaki (@zacky1972) 2019年5月3日
つまり Enum.reduce/3 に登場した acc なんです。
Enumの公式ドキュメントを和訳しても感じが掴みにくかったのですが、理解につなげることができました!
しかるに、
もし以上の連続ツイートを完全に理解できているならば,次のような関数を実装できると思います。
— Susumu Yamazaki (@zacky1972) 2019年5月3日
1 (第2引数) * 1 * 2 * 3 * 4 = 24
を計算する Enum.reduce/3 を使った式を書いてみてください。
以上が練習課題です。Let's challenge!
この式を考えていくと、1を起点に累積してどんどん、こんどは乗法しているように見えます。
iex > Enum.reduce([1,2,3,4],1,fn x,acc -> x * acc end)
— Kento Mizuno (@kmizuno0211) 2019年5月3日
24
です!
iex > Enum.reduce([1,2,3,4],1,fn x, acc -> x * acc end)
24
— Susumu Yamazaki (@zacky1972) 2019年5月3日
やった!
ということになります。
#追記
練習問題の式を、&記法とパイプライン演算子で書き換えてみようと思います。
iex > [1,2,3,4] |> Enum.reduce(1, & &1 * &2)
24
理解が固まったような気がします。
Elixirの学習効率の陰りについて、わたしは自分の発達障害や双極性障害、伴って起きている脳機能の障害じゃないか、と思い悶々としていました。
理解の礎が足りていないのではないかと思って、ほかの言語や教材に手を出しては、とっ散らかって余計パニックになってしまう有り様でした。
ようやく初学レベルにたどり着いたことと、おそらくはわたしへの指導に即して先生が公開ツイートの連投の形を取るこの学習法によって、なにかブレイクスルーが訪れないかなと思い、少なくともわたしはこれからにワクワクしています。
うまずたゆまず、頑張ります。
Kento Mizuno