Why is `Kernel.then/2` defined as a macro?

Just out of curiosity. Why is Kernel.then/2 a macro instead of a normal function?

Here’s the source code of Kernel.then/2:

defmacro then(value, fun) do
  quote do
    unquote(fun).(unquote(value))
  end
end

Here’s what I thought what the source code of Kernel.then/2 could be:

def then(value, fun) do
  fun.(value)
end
1 Like

Check out the pull request that did it. It includes reasons and a link to a mailing list discussion, too.

5 Likes

Thank you for the information.

I played with the idea a little.

defmodule Foo do
  defp thenf(value, fun) do
    fun.(value)
  end

  @compile {:inline, theni: 2}
  defp theni(value, fun) do
    fun.(value)
  end

  defmacrop thenm(value, fun) do
    quote do
      unquote(fun).(unquote(value))
    end
  end

  def ok1(val) do
    val
    |> thenf(&{:ok, &1})
  end

  def ok2(val) do
    val
    |> theni(&{:ok, &1})
  end

  def ok3(val) do
    val
    |> thenm(&{:ok, &1})
  end
end

Then I tried decompile the erlang byte code for ok1/1 which calls thenf/2 (normal function)

Foo
|> :code.get_object_code()
|> elem(1)
|> :beam_disasm.file()
|> elem(5)
|> Enum.find(fn tuple ->
  list = Tuple.to_list(tuple)
  match?([:function, :ok1 | _], list)
end)

and it provides

{:function, :ok1, 1, 11,
 [
   {:line, 1},
   {:label, 10},
   {:func_info, {:atom, Foo}, {:atom, :ok1}, 1},
   {:label, 11},
   {:test_heap, {:alloc, [words: 0, floats: 0, funs: 1]}, 1},
   {:make_fun3, {Foo, :"-ok1/1-fun-0-", 1}, 0, 109652265, {:x, 1}, {:list, []}},
   {:call_only, 2, {Foo, :thenf, 2}}
 ]}

You can clearly see it allocates an anonymous function.

I did the same thing for ok2/1 (calls theni/2 which is a inline function) and ok3/1 (calls thenm/2 which is a macro), and they provide

{:function, :ok2, 1, 13,
 [
   {:line, 2},
   {:label, 12},
   {:func_info, {:atom, Foo}, {:atom, :ok2}, 1},
   {:label, 13},
   {:test_heap, 3, 1},
   {:put_tuple2, {:x, 0}, {:list, [atom: :ok, x: 0]}},
   :return
 ]}

and

{:function, :ok3, 1, 15,
 [
   {:line, 3},
   {:label, 14},
   {:func_info, {:atom, Foo}, {:atom, :ok3}, 1},
   {:label, 15},
   {:test_heap, 3, 1},
   {:put_tuple2, {:x, 0}, {:list, [atom: :ok, x: 0]}},
   :return
 ]}

Looks like calling the inline function theni/2 and the macro thenm/2 generates the same VM instructions.

To be a comparable case you need to use theni from another module. As I recall inlining can work differently depending on whether you are inlining a function within the same module or from another module.

6 Likes

You are absolutely correct. Thank you.

When I put the callers and the callees into different modules, theni/2 generates the same instructions as thenf/2.

1 Like