Stream.chunk_while error when enumerating


I have a problem with Stream.chunk_while in combination with (elixir version is 1.5.1)

here is the code:

chunk_fun = fn
  i, [] ->
    {:cont, [i]}
  i, chunk ->
    if rem(i, 2) == 0 do
      {:cont, Enum.reverse(chunk), [i]}
      {:cont, [i | chunk]}
after_fun = fn
  [] -> {:cont, []}
  chunk -> {:cont, Enum.reverse(chunk), []}
stream = [1, 2, 3, 4, 5] |> Stream.chunk_while([], chunk_fun, after_fun)

calling stream |>
gives me this error:

** (ArithmeticError) bad argument in arithmetic expression
    (elixir) lib/enum.ex:793: anonymous fn/3 in Enum.fetch_enumerable/3
    (elixir) lib/stream.ex:1413: anonymous fn/3 in Enumerable.Stream.reduce/3
    (elixir) lib/stream.ex:234: Stream.after_chunk_while/3
    (elixir) lib/stream.ex:1446: Enumerable.Stream.do_done/2
    (elixir) lib/enum.ex:789: Enum.fetch_enumerable/3
    (elixir) lib/enum.ex:311:

but it’s ok to run stream with Enum.to_list:

> Enum.to_list(stream)
[[1], [2, 3], [4, 5]]

also it’s ok to get last chunk like so:

> stream |>
[4, 5]

Probably I’m doing something wrong?

1 Like is expecting the Enumerable.reduce to always call the reducer function with two params: the entry and a tuple which it passes through ({:not_found, index}) … however, when a :halt tuple is passed to Stream.chunk_while, it actually calls the after function (3rd parameter to Stream.chunk_while) and that calls the reducer function again!

So it is being run one too many times, and as a result instead of an index which it is incrementing with + 1, it gets (in your case, at least) a list. This is the source of the arithmetic error:

iex(127)> [1]+1
** (ArithmeticError) bad argument in arithmetic expression
    :erlang.+([1], 1)

(If your stream was a list of integers, it would also return an incorrect value, fwiw … it would eventually return a {:not_found, max_index} tuple, I believe…)

In HEAD, it does this differently and the search for the correct value almost works … but not quite. Instead if fails with:

** (CaseClauseError) no case clause matching: [[1], [2]]

Because it is now doing this:

  def at(enumerable, index, default \\ nil) do
    case slice_any(enumerable, index, 1) do
      [value] -> value
      [] -> default

… it is expecting a list with one entry at the end of its traversal and for the same source reason it doesn’t end up with that. oof.

Paging @josevalim … looks like a bone fide bug? :slight_smile:

1 Like

Yes, please file a bug report so we can include a fix on v1.6. :slight_smile:

1 Like