Strange error with nested Enum.reduce_while

For the following code

      c = %{a: %{}, b: [%{}, %{}]}

      r =
        Enum.reduce_while(c, nil, fn {_k, v}, _acc ->
          case v do
            %{} ->
              {:cont, nil}

            [%{} | _] = list ->
              Enum.reduce_while(list, nil, fn l, _acc ->
                {:cont, nil}
              end)
          end
        end)

The two reduce_while should both end with {:cont, nil} and r should be nil in the end.

However, I got an FunctionClauseError instead:

** (FunctionClauseError) no function clause matching in Enumerable.List.reduce/3    
    
    The following arguments were given to Enumerable.List.reduce/3:
    
        # 1
        []
    
        # 2
        nil
    
        # 3
        #Function<41.105768164/2 in :erl_eval.expr/6>
    
    Attempted function clauses (showing 4 out of 4):
    
        def reduce(_list, {:halt, acc}, _fun)
        def reduce(list, {:suspend, acc}, fun)
        def reduce([], {:cont, acc}, _fun)
        def reduce([head | tail], {:cont, acc}, fun)
    
    (elixir 1.16.0) lib/enum.ex:4839: Enumerable.List.reduce/3
    (elixir 1.16.0) lib/enum.ex:2582: Enum.reduce_while/3
    iex:4: (file)

The error message says the 2nd argument is nil (therefore, cannot match any clause). However, this should not be possible. Because a :cont tuple is guaranteed:

Any idea why it happens? Where does the bug locate, in my code, the standard lib, or even deeper?

The inner reduce_while here will unwrap {:cont, nil} and return nil.

2 Likes

Ah, yes. You got my blindspot! I didn’t think in this direction. I though this error is triggered when first entry, which led me to an wrong end. Thank you for saving my day!

I ran into this today, here’s the pattern I ended up with:

iex> Enum.reduce_while([[1, 2, 3, 4], [5, 6, 7, 8]], [], fn list, acc -> 
       Enum.reduce_while(list, {:cont, acc}, fn number, {:cont, acc} ->
         if number < 7 do
           {:cont, {:cont, [number | acc]}}
         else
           {:halt, {:halt, acc}}
         end
       end)
     end)

[6, 5, 4, 3, 2, 1]

The inner :cont/:halt tuples are ultimately for consumption by the outer reduce_while, but the inner reduce_while has to pass them along to itself.

This comprehension isn’t much shorter but is less confusing:

try do
  for list <- [[1, 2, 3, 4], [5, 6, 7, 8]], number <- list, reduce: [] do
    acc ->
      if number < 7 do
        [number | acc]
      else
        throw acc
      end
  end
catch
  x -> x
end