Stream.unfold/2を使いこなす〜簡単な無限／有限のループを作ってみる

今日は`Enum`, `Stream`, `Flow` を使いこなす上で，「隠れた」重要関数である `Stream.unfold/2` について説明します．用途としては無限数列を作る時，手続き型言語で書かれたループをElixirで書く時に使います．

# まず公式ドキュメントを読んでみる

https://hexdocs.pm/elixir/1.14/Stream.html#unfold/2

> `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 returns `nil`.
> 連続する値は、前のアキュムレータで `next_fun` を呼び出すことによって生成され、現在の値と次のアキュムレータを含むタプルを返す必要があります。 `nil` を返せば列挙終了。
>
> Examples
>
> ```elixir
> Stream.unfold(5, fn
>   0 -> nil
>   n -> {n, n - 1}
> end) |> Enum.to_list()
> [5, 4, 3, 2, 1]
> ```

これ読んでも容易には分かりませんね．私も最初はよく分かりませんでした．

# 無限カウントアップを `Stream.unfold/2` で作ってみる

次のようなコードで考えてみます．

```elixir
Stream.unfold(0, fn
  x -> {x + 1, x + 1}
end)
```

これは，無限に生成されるカウントアップの数列を作ることができます．

`Stream`を試す時，全般に言えることですが，`Stream`で生成される数列をリストにする時には，不用意に`Enum.to_list/1`を使わずに，`Enum.take/2`で10個くらい生成して様子を見る方が良いです．`Enum.to_list/1`だと無限に生成される場合に無限ループに陥ってしまいますので．

次のようにして生成結果を試します．

```elixir
Stream.unfold(0, fn
  x -> {x + 1, x + 1}
end)
|> Enum.take(10)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
```

意図通りっぽいですね．

次は，以下のようにしてみましょう．

```elixir
Stream.unfold(0, fn
  x -> {2 * (x + 1), x + 1}
end)
```

同じように`|> Enum.take(10)`をすると結果が次のようになります．

```elixir
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
```

わかりました？

もう1つ，次のようにしてみます．

```elixir
Stream.unfold(0, fn
  x -> {x, x + 1}
end)
```

続けて`|> Enum.take(10)`で結果は次のようになります．

```elixir
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```

感覚つかめました？

今度は次のようにしてみます．

```elixir
Stream.unfold(0, fn
  x -> {x, x + 2}
end)
```

続けて`|> Enum.take(10)`で結果は次のようになります．

```elixir
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
```

だいたいつかめましたかね．ダメ押しで次のようにしてみます．

```elixir
Stream.unfold(1, fn
  x -> {x, x * 2}
end)
```

すると`|> Enum.take(10)`で次のようになります．

```elixir
[1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
```

以上をまとめると次のような感じになります．

```elixir
Stream.unfold(カウンタ初期値, fn
  カウンタ変数 -> {リストに出力される各要素, カウンタの次の値}
end)
```

つまり，Cで無理やりかくと次のような感じのプログラムに対応します．

```c
int a[配列サイズ];
int i = カウンタ初期値;
while(1) {
  a[i] = リストに出力される各要素;
  i = カウンタの次の値;
}
```

数学で言うと，$x_n, x_{n + 1}$ で構成される漸化式で表される数列を生成できるということになります．

これで，`Stream.unfold/2`を使って簡単な無限ループが作れるようになりましたね！

# 有限カウントダウンを `Stream.unfold/2` で作ってみる

`Stream.unfold/2`では，有限カウントダウンのようにある値になった時に数列の生成を止めることもできます．それが公式ドキュメントのプログラム例です．

```elixir
Stream.unfold(5, fn
  0 -> nil
  n -> {n, n - 1}
end)
```

まずは恐る恐る `|> Enum.take(10)`を続けてみましょう．

```elixir
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`を使っても有限回数で停止します．

```elixir
Stream.unfold(5, fn
  0 -> nil
  n -> {n, n - 1}
end)
|> Enum.to_list()
[5, 4, 3, 2, 1]
```

色々値を変えて試してみてください．試す時には `Enum.take/2`を使った方が無難です．

こんな感じになります．

```elixir
Stream.unfold(初期カウンタ値, fn
  終わりのカウンタ値 -> nil # この時には生成するリストには追加されない
  カウンタ変数 -> {リストに出力される各要素, カウンタの次の値}
end)
```

Cで表すと次のようになります．

```c
int a[配列サイズ];
for(int i = 初期カウンタ値; i != 終わりのカウンタ値; i = カウンタの次の値) {
  a[i] = リストに出力される各要素;
}
```

注意点としては，カウンタの値が終わりのカウンタ値とピッタリ等しくならないと止まらないということです．
通常のCのループにあるような不等号で表す継続条件とはふるまいが異なりますので，注意してください．

では，Cの通常のループみたいにするにはどうしたらいいでしょうか．

```c
int a[配列サイズ];
for(int i = 初期カウンタ値; i < 終わりのカウンタ値; i = カウンタの次の値) {
  a[i] = リストに出力される各要素;
}
```

たぶんこんなふうにするのでしょうね．

```elixir
Stream.unfold(初期カウンタ値, fn
  カウンタ変数 -> cond do
    カウンタ変数 >= 終わりのカウンタ値 -> nil # 不等号を否定する点に注意
    true -> {リストに出力される各要素, カウンタの次の値}
  end
end)
```

次のように書いても良いです．

```elixir
Stream.unfold(初期カウンタ値, fn
  カウンタ変数 -> cond do
    カウンタ変数 < 終わりのカウンタ値 -> {リストに出力される各要素, カウンタの次の値}
    true -> nil
  end
end)
```

試しに次のようにしてみます．

```elixir
Stream.unfold(0, fn
  x -> cond do
    x >= 10 -> nil
    true -> {x, x + 1}
  end
end)
```

`|> Enum.to_list()`をつけると次の結果を得ます．

```elixir
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```

値を飛ばすようにしても確実に止まってくれます．

```elixir
Stream.unfold(0, fn
  x -> cond do
    x >= 10 -> nil
    true -> {x, x + 3}
  end
end)
```

これで，`Stream.unfold/2`を使って簡単な有限ループが作れるようになりましたね！

20241020 追記: `cond`を使わず，もっとシンプルに書けます！

```elixir
Stream.unfold(0, fn
  x when x >= 10 -> nil
  x -> {x, x + 3}
end)
```

```elixir
Stream.unfold(初期カウンタ値, fn
  カウンタ変数 when カウンタ変数 >= 終わりのカウンタ値 -> nil # 不等号を否定する点に注意
  カウンタ変数 -> {リストに出力される各要素, カウンタの次の値}
end)
```

```elixir
Stream.unfold(0, fn
  x when x < 10 -> {x, x + 3}
  _ -> nil
end)
```

```elixir
Stream.unfold(初期カウンタ値, fn
  カウンタ変数 when カウンタ変数 < 終わりのカウンタ値 -> {リストに出力される各要素, カウンタの次の値}
  _ -> nil
end)
```