Different ways to omit returned values

Is there any difference (in generated byte-code/performance/etc) between assigning the returned value of a function to _

_ = func()

and just calling it

func() # not assigned to anything, just called

?

I’ve noticed that it’s often done the first way (_ = func()) in some erlang code bases that I’ve been reading, like here, and I also remember this blogpost about some compiler optimizations like

The compiler will be able to optimize your code greatly, for example not creating the result [of a comprehension] if you don’t need it.

from https://ninenines.eu/articles/erlang-scalability/.

Does assigning a value to _ indicate to the compiler that the result is not needed?

As far as I know, the compiler finds

def foo() do
  fun()
  other_fun()
end

and

def foo() do
  _ = fun()
  other_fun()
end

are the same thing.

And to be honest, in my opinion, _ = fun() or any variant thereof is more readable than just fun().


However, I just ran the following test in IEx:

code1 = quote do
  defmodule Foo do
    def fun() do
      :ok
    end

    def test() do
      fun()
      nil
    end
  end
end

code2 = quote do
  defmodule Foo do
    def fun() do
      :ok
    end

    def test() do
      _ = fun()
      nil
    end
  end
end


compiled1 = Code.compile_quoted(code1)
compiled2 = Code.compile_quoted(code2)
compiled1 == compiled2
# false

Turns out they are not the same. Interesting! There probably is a way to make the compiled bytecode inspectable, giving us an idea of what the actual difference between the two is, but I don’t know how to do that right now.

1 Like

I was talking about this with @michalmuskala, who said that the final bytecode that is executed probably is the same, but since the bytecode also contains debug information that is different when you write the non-compiled code differently, they indeed are not equal.

I’ve not been able to uncompile the generated BEAM code to something readable myself yet. I know there are ways to do it (Michal shared this with me), but I have not got them to work thus far.

1 Like

They’re the same.

erlang

a() -> nil.
b() -> a().

compiled

i_func_info_IaaI 0 a a 0 
move_return_c nil 

i_func_info_IaaI 0 a b 0 
i_call_only_f a:a/0 

erlang

a() -> nil.
b() -> _ = a().

compiled

i_func_info_IaaI 0 b a 0 
move_return_c nil 

i_func_info_IaaI 0 b b 0 
i_call_only_f b:a/0 

I used :erts_debug.df/1. I don’t think micmus’s code will help here, as I believe it uses the debug info (which contains the _ =).

3 Likes

I don’t know how you access it from the elixir compiler but you can get the erlang compiler to return the BEAM assembler it generates. Quite illuminating. It is a bit easier to read than the output of :erts_debug.df/1.

1 Like

If you’re referring to 'S' for Elixir it should be ERL_COMPILER_OPTIONS="'S'" elixirc foo.ex. For reasons beyond me that causes a compilation error and puts the wrong module in foo.ex.S.

I thought :erts_debug.df/1 was easier to read in this case anyways.

Edit: this post helped explain the error above.