Weird/incorrect output from a with statement

This is a question that originated on the Elixir Slack from David Billskog and I have made only minor changes to the module.

Given this module:

defmodule Dummy do
  def execute(x) do
    with var_a <- get_a(x),
         var_b <- get_b(x) do
      IO.inspect(var_a, label: "a")
      IO.inspect(var_b, label: "b")
      IO.inspect({var_a, var_b})

      if var_a != var_b do
        IO.inspect(var_a, label: "a")
        IO.inspect(var_b, label: "b")
        IO.inspect({var_a, var_b})
      end
    end
  end
  defp get_a(x) do
    IO.puts("get a")
    if Enum.any?(x, & &1 == "A"), do: 3, else: 2
    |> IO.inspect(label: "returning a")
  end
  defp get_b(x) do
    IO.puts("get b")
    if Enum.any?(x, & &1 == "B"), do: 2, else: 1
    #|> IO.inspect(label: "returning b")
  end
end

Dummy.execute(["A", "B"])

I get this unexpected output:

$ elixir sample.exs
get a
get b
a: 3
b: 2
{3, 2}
a: 3
b: 1
{3, 1}

Does anyone understand what is happening here? I don’t expect for the value of b to change within the if statement, and I do expect the returning a inspect to be printed. Tested on elixir 1.11.2-otp-23 and erlang 23.0.2.

1 Like

I tested this with elixir 1.11.0 and Erlang 23.1 and I get a different result:

get a
get b
a: 3
b: 2
{3, 2}
a: 3
b: 2
{3, 2}

You probably need parenthesis around the if statement for the "returning a" part.

 defp get_a(x) do
    IO.puts("get a")
    (if Enum.any?(x, & &1 == "A"), do: 3, else: 2)
    |> IO.inspect(label: "returning a")
  end

But I’m not sure what happens with the output, seems strange that var_b change value. :thinking:

@axelson my hunch is that you are running into an Erlang or Elixir bug. Update to Erlang 23.1 and see if it persists. :slight_smile:

I just tried 23.1 (and 1.11.2) and can still reproduce the issue from slack. @axelson’s example doesn’t seem to cause issues for me.

❯❯❯❯ asdf local erlang 23.1                                                ~/Temp/test_elixir_problem
❯❯❯❯ iex                                                                   ~/Temp/test_elixir_problem
Erlang/OTP 23 [erts-11.1] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [hipe]

Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> defmodule Dummy do
...(1)>   def execute(x) do
...(1)>     with a <- get_a(x), b <- get_b(x) do
...(1)>       IO.inspect(b)
...(1)>       if a != b, do: IO.inspect(b)
...(1)>     end
...(1)>   end
...(1)>   defp get_a(x) do
...(1)>     if Enum.any?(x, & &1 == "A"), do: 3, else: 2 # always true
...(1)>   end
...(1)>   defp get_b(x) do
...(1)>     if Enum.any?(x, & &1 == "c"), do: 2, else: 1 # always true
...(1)>   end
...(1)> end
{:module, Dummy,
 <<70, 79, 82, 49, 0, 0, 8, 140, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 232,
   0, 0, 0, 25, 12, 69, 108, 105, 120, 105, 114, 46, 68, 117, 109, 109, 121, 8,
   95, 95, 105, 110, 102, 111, 95, 95, 10, ...>>, {:get_b, 1}}
iex(2)> Dummy.execute(["A", "c"])
2
1
1

Ah, yes you are correct about the reason the inspects are not printing out (I forgot about the precedence rules with |>).

I’m still seeing the issue with elixir 1.11.2-otp-23 and erlang 23.1.5

1 Like

We can check how the code was developed by using quote do

quote do
  if Enum.any?(x, & &1 == "A"), do: 3, else: 2
  |> IO.inspect(label: "returning a")
end
|> Macro.to_string()
|> IO.puts()

That results to

if(Enum.any?(x, &(&1 == "A"))) do
  3
else
  2 |> IO.inspect(label: "returning a")
end

Which is always true in this case. So, it won’t enter into else

3 Likes

Can confirm - Axelson’s example works fine on my machine (OTP23.1 / Elixir 1.11.2) and this one fails.

A colleague tried @LostKobrakai’s example on 22.2.6 / 1.11.1 and it printed 2 three times, as expected…

1 Like

Poked at it a little more. Strange things are afoot - I get different results depending on what’s on a line that never executes (OTP 23.1 / Elixir 1.11.2):

defmodule Dummy do
  def execute(x) do
    with var_a <- get_a(x),
         var_b <- get_b(x) do
      IO.inspect(var_a, label: "a")
      IO.inspect(var_b, label: "b")
      IO.inspect({var_a, var_b})

      if var_a != var_b do
        IO.inspect(var_a, label: "a")
        IO.inspect(var_b, label: "b")
        IO.inspect({var_a, var_b})
      end
    end
  end
  defp get_a(x) do
    IO.puts("get a")
    if Enum.any?(x, & &1 == "A") do
      3
    else
      2 |> IO.inspect(label: "nope")
    end
  end
  defp get_b(x) do
    IO.puts("get b")
    if Enum.any?(x, & &1 == "B"), do: 2, else: 1
  end
end

Dummy.execute(["A", "B"])
# prints
get a
get b
a: 3
b: 2
{3, 2}
a: 3
b: 2
{3, 2}

But commenting out |> IO.inspect(label: "nope") makes it do the {3,1} thing…

EDIT: bonus fact - the 1 has some kind of additional significance, changing it to anything else makes the bug stop happening on my machine.

1 Like

I simplified the example to this and can still reproduce the odd print running macOS Catalina 10.15.7 using Elixir 1.11.2 and Erlang 23.1.5.

defmodule Dummy do
  def execute(x) do
    with a <- get_a(x), b <- get_b(x) do
      IO.inspect(b)
      if a != b do
        IO.inspect(b)
      end
    end
  end
  defp get_a(x) do
    if x == "A", do: 3, else: 2
  end
  defp get_b(x) do
    if x == "A", do: 2, else: 1
  end
end
Dummy.execute("A")
# 2
# 1
# 1

Simplifying it one step furter, replacing if x == "A", do: 3, else: 2 (that will always evaluate to true) into if true, do: 3, else: 2 will fix the unexpected output.

defmodule Dummy do
  def execute(x) do
    with a <- get_a(x), b <- get_b(x) do
      IO.inspect(b)
      if a != b do
        IO.inspect(b)
      end
    end
  end
  defp get_a(x) do
    if true, do: 3, else: 2
  end
  defp get_b(x) do
    if x == "A", do: 2, else: 1
  end
end
Dummy.execute("A")
# 2
# 2
# 2
1 Like

Thanks for the isolated case. I was able to convert it to Erlang:

-module(dummy).

-compile([no_auto_import]).

-export([execute/1]).

execute(_x@1) ->
    _a@1 = get_a(_x@1),
    _b@1 = get_b(_x@1),
    erlang:display(_b@1),
    case _a@1 /= _b@1 of
        false ->
            nil;
        true ->
            erlang:display(_b@1),
            ok
    end.

get_a(_x@1) ->
    case _x@1 == <<"A">> of
        false ->
            2;
        true ->
            3
    end.

get_b(_x@1) ->
    case _x@1 == <<"A">> of
        false ->
            1;
        true ->
            2
    end.


And I see the same issue. I will open up a bug report.

Done: https://bugs.erlang.org/browse/ERL-1440

4 Likes

This also stops misbehaving if the code in the false branch of get_a is changed to something optimization-hostile:

-module(dummy).
-compile([no_auto_import]).
-export([execute/1]).

execute(_x@1) ->
    _a@1 = get_a(_x@1),
    _b@1 = get_b(_x@1),
    erlang:display(_b@1),
    case _a@1 /= _b@1 of
        false ->
            nil;
        true ->
            erlang:display(_b@1),
            ok
    end.
get_a(_x@1) ->
    case _x@1 == <<"A">> of
        false ->
            erlang:phash2(1,1);
        true ->
            3
    end.
get_b(_x@1) ->
    case _x@1 == <<"A">> of
        false ->
            1;
        true ->
            2
    end.

(inspired by the optimization barrier in GenServer and elsewhere)

Poking around with the example above turns up one difference between code that prints 2 1 versus 2 2 (pretty-printed beam_disasm):

   {:function, :execute, 1, 2,
    [
      {:label, 1},
      {:line, 1},
      {:func_info, {:atom, :dummy}, {:atom, :execute}, 1},
      {:label, 2},
      {:allocate_zero, 2, 1},
      {:move, {:x, 0}, {:y, 1}},
      {:line, 2},
      {:call, 1, {:dummy, :get_a, 1}},
      {:swap, {:y, 1}, {:x, 0}},
      {:line, 3},
      {:call, 1, {:dummy, :get_b, 1}},
      {:move, {:x, 0}, {:y, 0}},
      {:line, 4},
      {:call_ext, 1, {:extfunc, :erlang, :display, 1}},
      {:test, :is_eq, {:f, 3}, [y: 1, y: 0]},
      {:move, {:atom, nil}, {:x, 0}},
      {:deallocate, 2},
      :return,
      {:label, 3},
      # GOOD VERSION
      {:move, {:y, 0}, {:x, 0}},
      {:trim, 2, 0},
      # BAD VERSION
      {:trim, 2, 0},
      {:move, {:integer, 1}, {:x, 0}},
      # identical from here
      {:line, 5},
      {:call_ext, 1, {:extfunc, :erlang, :display, 1}},
      {:move, {:atom, :ok}, {:x, 0}},
      {:deallocate, 0},
      :return
    ]},

Something is pulling that 1 out of the ether…