Stream で遊んでみる(ついでに String でも遊んでみる)
いやあ、遅延評価は面白い。ということで、はじめてなElixir(2)の続きをやるはずが、ちょっと寄り道して Stream で遊んでみます。備忘録の色彩が強いです。
- Haskell: 片っ端から遅延評価
- Elixir: 一部が遅延評価
- Rust: 全部 Eager Eval
ということらしいので、IoT用に時間順序を気にしながら、それでも遅延評価を使いたい向きには…
Elixir しかないんじゃないでしょうか?!
無限リストであそぶ
@zacky1972 先生のご紹介で [@HirofumiTmamori さんによる「翻訳 Elixirのストリーム」] (https://qiita.com/HirofumiTamori/items/abf9a9478bfc1161000c) を見ながらやってみますね。ただ、これ ver が古いらしく、そのままで動かないのもあります。なお原文はこちらなようです。
Elixir Streams by Drew Olson
まず同じ要素の無限リスト
iex(1)> Stream.repeatedly(fn -> 0 end) |> Enum.take(5)
[0, 0, 0, 0, 0]
これじゃあ、同じ要素しかできないじゃんとお嘆きの方にはこちらを。
iex(2)> Stream.repeatedly(fn -> Enum.random(0..9) end) |> Enum.take(5)
[8, 6, 3, 2, 0]
まあ、素敵じゃあないですか。ちゃんと「嘘のサンパチ」も出てきてるし(たまたまだけど)。ちなみに私、「花のサンパチ」生まれでございます。以後お見知りおきを。
ついでにちなみになんで最初の5要素かというと take(5) ってぐらいで。
The Dave Brubeck Quartet
自然数列
つぎは簡単なところで自然数列を作ってみます。
iex(7)> Stream.iterate(0, fn(n) -> n + 1 end) |> Enum.take(5)
[0, 1, 2, 3, 4]
これはこうも書けます。
iex(6)> Stream.iterate(0, & (&1 + 1)) |> Enum.take(5)
[0, 1, 2, 3, 4]
これを集合と思えば自然数 $\omega$ を表しているってことになりますね。
偶数列を作ってみる
自然数列の応用になります。こうなるといくつか書きようが出てきます。
まず、さっきの延長で +2 ずつの列を作ると考えるとこんな。
iex(9)> Stream.iterate(0, fn(n) -> n + 2 end) |> Enum.take(5)
[0, 2, 4, 6, 8]
奇数を除いてしまえと思えば。
iex(11)> Stream.iterate(0, &(&1+1)) |> Stream.reject(&(rem(&1,2) != 0)) |> Enum.take(5)
[0, 2, 4, 6, 8]
偶数だけいただこうと思えば。
iex(12)> Stream.iterate(0, &(&1+1)) |> Stream.filter(&(rem(&1,2) == 0)) |> Enum.take(5)
[0, 2, 4, 6, 8]
自然数列の各要素を2倍にすると思えば。
iex(3)> Stream.iterate(0, &(&1+1)) |> Stream.map(&(&1 * 2)) |> Enum.take(5)
[0, 2, 4, 6, 8]
あ、プロンプトの番号が若返ってるのを見れば一度暴走させたらしいことが分かりますね (|> 見ないふり)。
2で割らなくても、Integer モジュールを持ってくると奇遇チェックの関数が使えます。
iex(4)> require Integer
Integer
とまずはやっておいて
iex(6)> Stream.iterate(0, &(&1+1)) |> Stream.filter(&(! Integer.is_odd(&1))) |> Enum.take(5)
[0, 2, 4, 6, 8]
とか
iex(7)> Stream.iterate(0, &(&1+1)) |> Stream.filter(&(Integer.is_even(&1))) |> Enum.take(5)
[0, 2, 4, 6, 8]
しかし、Integerモジュールって標準じゃないのかな。ちなみに rem 関数は Integer モジュールでは Integer.mod のようで、なぜここで名前を変えるかなぁ。
その次の数をプログラムで書く
柔軟にストリームを生成したいなら Stream.unfold を使うようです。fold が畳み込む感じですから、unfold ってのは遥か彼方に展開していくようなイメージでしょうか。
iex(16)> Stream.unfold(0, fn n -> {n, 0} end) |> Enum.take(5)
[0, 0, 0, 0, 0]
初期値と「その次を計算する関数」を unfold が受け取ります。
iex(19)> Stream.unfold(0, fn n -> {n, n + 1} end) |> Enum.take(5)
[0, 1, 2, 3, 4]
これは自然数列ですが、偶数列にするのも同じノリです。
iex(20)> Stream.unfold(0, fn n -> {n, n + 2} end) |> Enum.take(5)
[0, 2, 4, 6, 8]
列の直前の最後の要素ではなく複数の要素を受け取って次の要素を計算するようにもできます。次は fibonacci 数列。
iex(22)> Stream.unfold({1, 1}, fn {x, y} -> {x, {y, x + y}} end) |> Enum.take(5)
[1, 1, 2, 3, 5]
これは流石に最後の10要素を見たくなりますね。
iex(25)> Stream.unfold({1, 1}, fn {x, y} -> {x, {y, x + y}} end) |> Enum.take(10)
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
直前の3要素を足すような数列ならこんな風に書きます。
iex(26)> Stream.unfold({1, 1, 1}, fn {x, y, z} -> {x, {y, z, x + y + z}} end) |> Enum.take(10)
[1, 1, 1, 3, 5, 9, 17, 31, 57, 105]
同じノリで0の列を作るならこうも書けます。
iex(32)> Stream.unfold({0, 0}, fn {x, y} -> {x, {y, 0}} end) |> Enum.take(5)
[0, 0, 0, 0, 0]
もっとひどい書き方でこうも書けます。いやはや。
iex(35)> Stream.unfold({0, 0}, fn {_x, _y} -> {0, {0, 0}} end) |> Enum.take(5)
[0, 0, 0, 0, 0]
リテラルで遊んでみる
0..9 ってので数列を作れます。正確には整数のリストを作れます。
iex(36)> 0..9
0..9
これだとオウム返しで何が得られたのかよくわからないので、リストを切り出してみます。
iex(37)> 0..9 |> Enum.take(0)
[]
iex(38)> 0..9 |> Enum.take(1)
[0]
iex(39)> 0..9 |> Enum.take(2)
[0, 1]
iex(40)> 0..9 |> Enum.take(5)
[0, 1, 2, 3, 4]
じゃあ、これで無限リストと思ってこう書いてもダメです。
iex(42)> 0.. |> Enum.at(5)
** (SyntaxError) iex:42: syntax error before: '|>'
0.. で0から始まる無限リストとは解釈されません。残念。
文字列定数を作る
"0123456789" なる文字列を作りたいとします。"0..9" なんて書いてもダメです。これは4文字からなる文字列に過ぎないので。で、こんなのを考えてみました。
iex(42)> Enum.to_list(0..9) |> Enum.map(& Integer.to_string(&1)) |> Enum.map_join(& &1)
"0123456789"
なんか大仰ですね。それにこれは "abcdefghijklmnopqrstuvwxyz"を生成できません。?を使うとASCIIコード(正確にはutf8コードと言うべきか)が取れますので、こんな風に書けると教えてもらいました。
iex(49)> ?0..?9
48..57
iex(50)> ?0..?9 |> Enum.map(& String.Chars.to_string([&1]))
["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
iex(51)> ?0..?9 |> Enum.map(& String.Chars.to_string([&1])) |> Enum.join()
"0123456789"
これも @zacky1972 先生に教えてもらったものです。アルファベットならこうします。
iex(52)> ?a..?z |> Enum.map(& String.Chars.to_string([&1])) |> Enum.join()
"abcdefghijklmnopqrstuvwxyz"
大文字にするならこれを流用しても良いですが String.upcase 関数も使えます。
iex(53)> ?a..?z |> Enum.map(& String.Chars.to_string([&1])) |> Enum.join() |> String.upcase()
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
数字列と小文字列をくっつけて生成してみましょう。
iex(58)> Enum.concat(?0..?9, ?a..?z) |> Enum.map(& String.Chars.to_string([&1])) |> Enum.join()
"0123456789abcdefghijklmnopqrstuvwxyz"
これに upcase かけると小文字にだけ適用されてのぞみのものが得られます。
iex(59)> Enum.concat(?0..?9, ?a..?z) |> Enum.map(& String.Chars.to_string([&1])) |> Enum.join() |> String.upcase()
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
#参考文献
Elixir v1.7.3
[@HirofumiTmamori さんによる「翻訳 Elixirのストリーム」] (https://qiita.com/HirofumiTamori/items/abf9a9478bfc1161000c)
はじめてなElixir(2)