今日はEnum
, Stream
, Flow
を使いこなす上で,「隠れた」重要関数である Stream.unfold/2
について説明します.用途としては無限数列を作る時,手続き型言語で書かれたループをElixirで書く時に使います.
まず公式ドキュメントを読んでみる
unfold(next_acc, next_fun)
@spec unfold(acc(), (acc() -> {element(), acc()} | nil)) :: Enumerable.t()
Emits a sequence of values for the given accumulator.
指定されたアキュムレータの一連の値を発行します。Successive values are generated by calling
next_fun
with the previous accumulator and it must return a tuple with the current value and next accumulator. The enumeration finishes if it returnsnil
.
連続する値は、前のアキュムレータでnext_fun
を呼び出すことによって生成され、現在の値と次のアキュムレータを含むタプルを返す必要があります。nil
を返せば列挙終了。Examples
Stream.unfold(5, fn 0 -> nil n -> {n, n - 1} end) |> Enum.to_list() [5, 4, 3, 2, 1]
これ読んでも容易には分かりませんね.私も最初はよく分かりませんでした.
無限カウントアップを Stream.unfold/2
で作ってみる
次のようなコードで考えてみます.
Stream.unfold(0, fn
x -> {x + 1, x + 1}
end)
これは,無限に生成されるカウントアップの数列を作ることができます.
Stream
を試す時,全般に言えることですが,Stream
で生成される数列をリストにする時には,不用意にEnum.to_list/1
を使わずに,Enum.take/2
で10個くらい生成して様子を見る方が良いです.Enum.to_list/1
だと無限に生成される場合に無限ループに陥ってしまいますので.
次のようにして生成結果を試します.
Stream.unfold(0, fn
x -> {x + 1, x + 1}
end)
|> Enum.take(10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
意図通りっぽいですね.
次は,以下のようにしてみましょう.
Stream.unfold(0, fn
x -> {2 * (x + 1), x + 1}
end)
同じように|> Enum.take(10)
をすると結果が次のようになります.
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
わかりました?
もう1つ,次のようにしてみます.
Stream.unfold(0, fn
x -> {x, x + 1}
end)
続けて|> Enum.take(10)
で結果は次のようになります.
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
感覚つかめました?
今度は次のようにしてみます.
Stream.unfold(0, fn
x -> {x, x + 2}
end)
続けて|> Enum.take(10)
で結果は次のようになります.
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
だいたいつかめましたかね.ダメ押しで次のようにしてみます.
Stream.unfold(1, fn
x -> {x, x * 2}
end)
すると|> Enum.take(10)
で次のようになります.
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
以上をまとめると次のような感じになります.
Stream.unfold(カウンタ初期値, fn
カウンタ変数 -> {リストに出力される各要素, カウンタの次の値}
end)
つまり,Cで無理やりかくと次のような感じのプログラムに対応します.
int a[配列サイズ];
int i = カウンタ初期値;
while(1) {
a[i] = リストに出力される各要素;
i = カウンタの次の値;
}
数学で言うと,$x_n, x_{n + 1}$ で構成される漸化式で表される数列を生成できるということになります.
これで,Stream.unfold/2
を使って簡単な無限ループが作れるようになりましたね!
有限カウントダウンを Stream.unfold/2
で作ってみる
Stream.unfold/2
では,有限カウントダウンのようにある値になった時に数列の生成を止めることもできます.それが公式ドキュメントのプログラム例です.
Stream.unfold(5, fn
0 -> nil
n -> {n, n - 1}
end)
まずは恐る恐る |> Enum.take(10)
を続けてみましょう.
Stream.unfold(5, fn
0 -> nil
n -> {n, n - 1}
end)
|> Enum.take(10)
[5, 4, 3, 2, 1]
Enum.take/2
で要素10個要求したのに5個だけ返ってきました.これはStream.unfold/2
で5個という有限の個数だけ生成されたことを意味します.
こういう状況だと,Enum.to_list/1
を使っても有限回数で停止します.
Stream.unfold(5, fn
0 -> nil
n -> {n, n - 1}
end)
|> Enum.to_list()
[5, 4, 3, 2, 1]
色々値を変えて試してみてください.試す時には Enum.take/2
を使った方が無難です.
こんな感じになります.
Stream.unfold(初期カウンタ値, fn
終わりのカウンタ値 -> nil # この時には生成するリストには追加されない
カウンタ変数 -> {リストに出力される各要素, カウンタの次の値}
end)
Cで表すと次のようになります.
int a[配列サイズ];
for(int i = 初期カウンタ値; i != 終わりのカウンタ値; i = カウンタの次の値) {
a[i] = リストに出力される各要素;
}
注意点としては,カウンタの値が終わりのカウンタ値とピッタリ等しくならないと止まらないということです.
通常のCのループにあるような不等号で表す継続条件とはふるまいが異なりますので,注意してください.
では,Cの通常のループみたいにするにはどうしたらいいでしょうか.
int a[配列サイズ];
for(int i = 初期カウンタ値; i < 終わりのカウンタ値; i = カウンタの次の値) {
a[i] = リストに出力される各要素;
}
たぶんこんなふうにするのでしょうね.
Stream.unfold(初期カウンタ値, fn
カウンタ変数 -> cond do
カウンタ変数 >= 終わりのカウンタ値 -> nil # 不等号を否定する点に注意
true -> {リストに出力される各要素, カウンタの次の値}
end
end)
次のように書いても良いです.
Stream.unfold(初期カウンタ値, fn
カウンタ変数 -> cond do
カウンタ変数 < 終わりのカウンタ値 -> {リストに出力される各要素, カウンタの次の値}
true -> nil
end
end)
試しに次のようにしてみます.
Stream.unfold(0, fn
x -> cond do
x >= 10 -> nil
true -> {x, x + 1}
end
end)
|> Enum.to_list()
をつけると次の結果を得ます.
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
値を飛ばすようにしても確実に止まってくれます.
Stream.unfold(0, fn
x -> cond do
x >= 10 -> nil
true -> {x, x + 3}
end
end)
これで,Stream.unfold/2
を使って簡単な有限ループが作れるようになりましたね!
20241020 追記: cond
を使わず,もっとシンプルに書けます!
Stream.unfold(0, fn
x when x >= 10 -> nil
x -> {x, x + 3}
end)
Stream.unfold(初期カウンタ値, fn
カウンタ変数 when カウンタ変数 >= 終わりのカウンタ値 -> nil # 不等号を否定する点に注意
カウンタ変数 -> {リストに出力される各要素, カウンタの次の値}
end)
Stream.unfold(0, fn
x when x < 10 -> {x, x + 3}
_ -> nil
end)
Stream.unfold(初期カウンタ値, fn
カウンタ変数 when カウンタ変数 < 終わりのカウンタ値 -> {リストに出力される各要素, カウンタの次の値}
_ -> nil
end)