Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Fun with macros

macro.livemd

Fun with macros

Section

background A macro is like the upside down world - it’s simlar to the existing world but different in some way. The changes you make in upside down are reflected in the real world. Lets have a look at some examples:

a = 1 + 1
b = a * 2
4
quote do
  a = 1 + 1
  b = a * 2
end
{:__block__, [],
 [
   {:=, [],
    [{:a, [], Elixir}, {:+, [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]], [1, 1]}]},
   {:=, [],
    [{:b, [], Elixir}, {:*, [context: Elixir, imports: [{2, Kernel}]], [{:a, [], Elixir}, 2]}]}
 ]}

what it means:

a = 1 + 1 expands to:

{:=, [] = context, [left, right]}

left expands to {:a, [] = context, Elixir} and it is just a name of the variable.

right is 1+1 and it’s {:+, context, [left, right]} where left and right both equal to 1. The context [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]] just says that + is a function from Kernel and there is +/1 and +2 arity. It means you can use + also as one arity function +1 which is the same as 1

What’s the difference between := and :+? := did not have such context. It’s because = is a special form not a function, this is not important for us right now. To run the quoted code we can use Code.eval_quoted/2

quote do
  a = 1 + 1
  b = a * 2
end
|> Code.eval_quoted()
{4, [{{:b, Elixir}, 4}, {{:a, Elixir}, 2}]}

This returns the result and env where a=2 and b=4 Knowing this we can try to maniplate the ast:

quote do
  a = 1 + 1
  b = a * 2
end
|> then(fn {:__block__, [], expressions} ->
  modified_expressions =
    Enum.map(
      expressions,
      fn {:=, [], [left, {operator, context, [l, r]} = _right]} ->
        {:=, [], [left, {operator, context, [l, r * 2]}]}
      end
    )

  {:__block__, [], modified_expressions}
end)
|> Code.eval_quoted()
{12, [{{:b, Elixir}, 12}, {{:a, Elixir}, 3}]}

what we did here is to mulitply the last integer by 2

a = 1 + 1 becomes a = 1 + 2 and

b = a * 2 becomes b = a * 4

3 + 4 is 12 and thats what we can see in the values returned by Code.eval_quoted()

{12, [{{:b, Elixir}, 12}, {{:a, Elixir}, 3}]}

That’s a simple example but where it can be useful?

Elixir has the dbg() macro that modifies the ast to display intermediate results:

"hello world"
|> String.split()
|> Enum.map(&String.upcase/1)
|> Enum.join(" ")
|> dbg()
[livemd/macro.livemd#cell:75yd7uy4siislwes:5: (file)]
"hello world" #=> "hello world"
|> String.split() #=> ["hello", "world"]
|> Enum.map(&String.upcase/1) #=> ["HELLO", "WORLD"]
|> Enum.join(" ") #=> "HELLO WORLD"

"HELLO WORLD"
defmodule DBG do
  def dbg([do: {op, meta, clauses}], options, env) do
    [do: dbg({op, meta, clauses}, options, env)]
  end

  def dbg({op, meta, clauses}, options, env) when op in [:__block__, :def, :defmodule] do
    clauses = Enum.map(clauses, &dbg(&1, options, env))
    {op, meta, clauses}
  end

  @kernel (Kernel.SpecialForms.__info__(:macros) ++
             Kernel.__info__(:macros) ++ Kernel.__info__(:functions))
          |> Keyword.keys()
  def dbg({op, meta, _data} = ast, _options, _env) when op in @kernel do
    label = ast |> Macro.to_string() |> String.replace(~r/\s\s+/, " ")
    label = "line #{meta[:line]}: " <> label
    {{:., meta, [{:__aliases__, meta, [:IO]}, :inspect]}, meta, [ast, [label: label]]}
  end

  def dbg({_op, _, _} = ast, _, _) do
    ast
  end
end

Application.put_env(:elixir, :dbg_callback, {DBG, :dbg, []})
:ok
defmodule XXX do
  def function(_x) do
    a = 1
    b = 3
    d = a + b

    c =
      if a == 2 do
        2
      else
        b
      end

    i =
      with e <- 1 + c,
           f = b * e do
        f + e
      end

    for g <- a..d, h <- b..c do
      g + h + i
    end
  end

  def fun(a, b) do
    a + b
  end
end
|> dbg()

XXX.function(1)
XXX.fun(1, 2)
line 1: XXX: XXX
line 3: a = 1: 1
line 4: b = 3: 3
line 5: d = a + b: 4
line 7: c = if a == 2 do 2 else b end: 3
line 14: i = with e <- 1 + c, f = b * e do f + e end: 16
line 20: for g <- a..d, h <- b..c do g + h + i
end: [20, 21, 22, 23]
line 26: a + b: 3
3