技術メモ

プログラミングとか電子工作とか

Elixir入門(第十章 Enumerables and Streams)

f:id:ysmn_deus:20190122112104p:plain

どうも、靖宗です。
今回はEnumerablesとStreams。列挙型とストリーム?
よく分からないので読み進めていきます。

Enumerables

今までに学んでいるリストとマップが"Enumerables"に該当するようです。
ElixirにはEnumモジュールというのがあるそうで、リストとマップに対していろんな操作ができるようです。

iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
[2, 12]

そういえば前回の再帰でreduceやmap操作をやりましたが、Enumモジュールはこれらの操作も可能。

iex> Enum.reduce([1, 2, 3], 0, fn(x, acc) -> x + acc end)
6
iex> Enum.map([1, 2, 3], fn(x) -> x * 2 end)
[2, 4, 6]

さらに関数キャプチャを使うと上のやつはこう書ける。
今までの集大成っぽい。

iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.reduce(1..3, 0, &+/2)
6

ちなみに見慣れない表現数字A..数字Bというのでたぶん数字A~数字Bのリストが表現される。レンジ(range)と言うそうです。
ただし、それ単体ではリストとしては存在してない?

iex(2)> 1..2
1..2

Enum操作やなんかの時にいちいち範囲のリストを書いたりしなくて良いのは便利そう。

Enumモジュールはリストやマップに対して再帰的な操作などをする関数群で、リストの挿入とか更新とかはListモジュールとか使ってね!とのこと。
モジュール毎に役割を明確に分けているようです。
プロトコルの話とか書いてあるけどよくわかんないので次!

Eager vs Lazy

Enumモジュールの関数は全ては熱心である。」
('ω')。o(????????????)

遅延評価とかの話でしょうか?
兎に角読み進めます。

iex> odd? = &(rem(&1, 2) != 0)
#Function<6.80484245/1 in :erl_eval.expr/5>
iex> Enum.filter(1..3, odd?)
[1, 3]

Enum.filerは(無名)関数を第二引数に放り込めば第一引数をフィルタリングしてそのリストを返してくれる関数のようです。

iex> total_sum = 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum
7500000000

Qiitaで見たことある記号|>だ!
パイプライン演算子と言うモノらしく、先ほどの例と比較すると左辺の結果を右辺の第一引数に放り込む演算子っぽいです。
たしかにやってる処理を考えるとシンプルになってますね。
あと、もしかしたらどっかで既に出てたかもしれませんが、カンマの代わりに_で桁を区切って見やすくするみたいですね。100,000 -> 100_000
個人的には日本人になじみがないので若干分かりにくいですが。

The pipe operator(パイプ演算子

|>はパイプ(パイプライン?)演算子という名前で、上記に書いた「左辺の結果を右辺の第一引数に放り込む演算子」のようです。
おおむねUnix|と認識して良さそうです。
やはり、概ねコードの可読性を上げるもののようで、上記の例をパイプ演算子無しで表現すると

iex> Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
7500000000

となります。確かに分かりづらい・・・

Streams(ストリーム)

今までのEnumモジュールに対して、Streamモジュールは怠惰な操作に対応している。
('ω')。o(????????????)

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
7500000000

得られる結果は同じ。
おそらく、計算が実行されるタイミングが違うのでしょう。

iex> 1..100_000 |> Stream.map(&(&1 * 3))
#Stream<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]>

やはりそうです。
Streamモジュールにリストを入れた段階では入力と処理の関数が定義されたデータ構造が返ってきているようです。

iex(5)> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
#Stream<[
  enum: 1..100000,
  funs: [#Function<49.117072283/1 in Stream.map/2>,
   #Function<41.117072283/1 in Stream.filter/2>]
]>

Streamモジュールをパイプ演算子で何個かつなげても同じ。
最初のリストが保持されている状況を鑑みるに、最終的な結果を得る瞬間に計算するようです。
遅延評価ですね。

Streamモジュールもenumerableを操作するモジュールの用で、おそらくEnumの遅延評価版モジュールだと思えば良さそうです。
ただし、遅延評価ならではの関数もあるっぽく、Stream.cycle/1は受けたenumerableを永遠に周回して返す関数のようです。

iex> stream = Stream.cycle([1, 2, 3])
#Function<15.16982430/2 in Stream.unfold/2>
iex> Enum.take(stream, 10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

Stream.unfold/2は上の繰り返さない版?

iex(6)> stream = Stream.unfold("hełło", &String.next_codepoint/1)
#Function<65.117072283/2 in Stream.unfold/2>
iex(7)> Enum.take(stream, 3)
["h", "e", "?"]
iex(8)> Enum.take(stream, 5)
["h", "e", "?", "?", "o"]
iex(9)> Enum.take(stream, 8)
["h", "e", "?", "?", "o"]
iex(10)> Enum.take(stream, 1000000)
["h", "e", "?", "?", "o"]

Win機なので文字化けしてますがご容赦ください。
どんな大きな個数でとりだそうとしても初期値以上のリストは返ってこないようです。
普通のリストとかでエラー起こるのかな

iex(12)> Enum.take([1, 2, 3, 4], 5)
[1, 2, 3, 4]

起こらん。文字列をリスト化したいときに使うんだろうか。用途は不明。

ファイルを開くときにStream.resource/3という関数が訳に立つとのこと。

iex> stream = File.stream!("path/to/file")
#Function<18.16982430/2 in Stream.resource/3>
iex> Enum.take(stream, 10)

直接的には使ってないけどFile.stream!という箇所で内部の処理で使われているのであろう。
大きいファイルを全部メモリ上などに展開してしまうとリソースの無駄では済まず、展開すらできない状況などもあるのでそういうときに使うのかな?
あとは、逐次ネットワークからデータを拾ってくるなどだろうか。

訳分からんやろうから最初はEnum使って慣れてきたらStream使い!とのこと。なるほど。
基本的にはStreamは遅延評価が必要な場合に使用し、初心者はEnumで頑張っとけばよさそう。
まぁ、折角Elixir使うんだったらこういうのやりたい!って案件が上がってきたら不可避だとは思いますが・・・