技術メモ

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

Elixir入門(第十七章 Comprehensions)

f:id:ysmn_deus:20190122112104p:plain

どうも、靖宗です。
今回は「Comprehensions」。もう単語の意味もよく分かりません・・・(包容力?包含?コンプリヘンション?)

どうやらComprehensionsはリストなどの再帰ループの糖衣構文のようです。
今まで出てこなかったforについて言及されてます。
もはやここまで来るとなじみ深いforが出てくる方が不気味です。
さっそく例を見ていきます。

iex> for n <- [1, 2, 3, 4], do: n * n
[1, 4, 9, 16]

あえてEnumで書くならEnum.map([1, 2, 3, 4], fn x -> x * x end)といったところでしょうか。
Comprehensionはgeneratorsfilterscollectablesの3要素でできているようです。

Generators and filters(ジェネレータとフィルター)

先ほどの例でいうと、n <- [1, 2, 3, 4]がジェネレータに該当するようです。
ここではリストが書かれてますが、rangeなども使える様です。

iex> for n <- 1..4, do: n * n
[1, 4, 9, 16]

キーワードリストなどとのパターンマッチも使用可能

iex> values = [good: 1, good: 2, bad: 3, good: 4]
iex> for {:good, n} <- values, do: n * n
[1, 4, 16]

パターンマッチだけで色々フィルタリングできそうな気はしますが、より細かい条件付けなどの場合はフィルターの方が有用かもしれません。
フィルターはジェネレータの後にかくようです。

iex> multiple_of_3? = fn(n) -> rem(n, 3) == 0 end
iex> for n <- 0..5, multiple_of_3?.(n), do: n * n
[0, 9]

フィルターに記載した関数がtrueの時のみの結果となってます。
フィルターの関数がfalsenil以外の時の値を採用するようです。

このジェネレータやフィルターを用いるとEnumStreamよりも簡潔に書けるとのことですが、どうなんでしょうか・・・
ケースバイケースですかね。とりあえずサンプルを見ます。

dirs = ['/home/mikey', '/home/james']
for dir  <- dirs,
    file <- File.ls!(dir),
    path = Path.join(dir, file),
    File.regular?(path) do
  File.stat!(path).size
end

('ω')。o(????????????)
|>で紡いでいったほうが分かりやすそうな気がしますが、丁寧に1個1個みていくとdir <- dirsfile <- File.ls!(dir)がジェネレータに該当し、path = Path.join(dir, file)File.regular?(path)がフィルターに該当するといったところでしょうか。
path = Path.join(dir, file)に関してはタダの変換やんと言いたくなりますが、そういうもんなんでしょう。

複数ジェネレータを書いた場合は後に書いた方が先に回る仕組みみたいです。

iex> for i <- [:a, :b, :c], j <- [1, 2], do:  {i, j}
[a: 1, a: 2, b: 1, b: 2, c: 1, c: 2]

他の言語でforがネストしてるイメージでしょうか。

あとはcomprehension内部で使用した変数などは外部に影響しないとのこと。これはまぁそうですよねという話な気はします。

Bitstring generators

Bitstring generatorsなるものの説明が。bitstringのストリームで役にたつで!とのこと。サンプルを見ます。

iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]

bitstringできたらこう書きましょうって感じでしょうか。
string系列はじゃっかんヤヤコシイので一回復習しないとあかんかな。

The :into option

基本的に先ほどまでの例ではcomprehensionはリストを返してましたが:intoオプションを利用することでmapや文字列に放り込めるそうです。
とりあえずサンプルを見ていきます。

iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
"helloworld"

bitstringをスペースであるかの成否でフィルタリングして文字列に放り込むといったところでしょうか。
おそらくinto:""で文字列に対する変換の処理が暗示的になされている気がします。

mapへの:intoオプションの例。

iex> for {key, val} <- %{"a" => 1, "b" => 2}, into: %{}, do: {key, val * val}
%{"a" => 1, "b" => 4}

ここでもintoのオプションで変換先がmapである事が明示されています。
おそらくココでプロトコルのような実装がなされていて、暗示的にリストからmapへの変換がなされている?
Collectableプロトコルに放り込まれているようなので一応見てみる。

github.com

defimpl Collectable, for: List do
  def into(original) do

...

やっぱりそうっぽい。
あまり深入りせずサンプルのようなケースを覚えて行った方が生産的でしょうか。

リストはEnumerablesなので、遅延評価する場合は:intoオプションでストリームに放り込むこともできるみたい。

iex> stream = IO.stream(:stdio, :line)
iex> for line <- stream, into: stream do
...>   String.upcase(line) <> "\n"
...> end

ただし上のサンプルは実行すると永遠に終わらないと思います。
このへんは使っていくウチに慣れるか、リストとして結果が受けれるので結局パイプ演算子で垂れ流すかするのが良さそうな気はします。