Trying to define my own if macro

defmodule Control do
  defmacro my_if(expr, do: if_block, else: else_block) do
    quote do
      Control.do_my_if(unquote(expr), do: unquote(if_block), else: unquote(else_block))
    end
  end

  defmacro my_if(expr, do: if_block) do
    quote do
      Control.do_my_if(unquote(expr), do: unquote(if_block), else: nil)
    end
  end

  def do_my_if(expr, do: if_block, else: else_block) do
    case (expr) do
        result when result in [false, nil] -> else_block
        _ -> if_block
    end
  end
end
iex(1)> c "control.exs"
[Control]
iex(2)> require Control
Control
iex(3)> Control.my_if(2==2, do: "true", else: "false")
"true"

What i don’t like about my implementation is that i would like to keep do_my_if as private function but this doesn’t compile if i do that.
I also don’t like the fact that i have to call Control.do_my_if in the macro body, why can’t i simply do do_my_if. During the AST expansion phase do_my_if should be totally legal shouldn’t it?

I looked at the Elixir source code https://github.com/elixir-lang/elixir/blob/5feec03db6a134371d9c0f60cc8873232659005e/lib/elixir/lib/kernel.ex and the two points i mentioned seemed achievable. I don’t know what i am doing wrong. Any help is appreciated, thanks in advance.

Your macro’s are calling do_my_if in the ‘callers’ scope, not the macro scope. This is proven by showing that both branches are taken:

iex> import Control
Control
iex> my_if(2==2, do: IO.inspect(true), else: IO.inspect(false))
true
false
true

You can change it to (and should, you cannot make it private because anything a defmacro calls (actually calls, not inside a quote) needs to be publicly accessible, I’m not sure why but they do) by changing your module to be:

defmodule Control do
  defmacro my_if(expr, do: if_block, else: else_block) do
    Control.do_my_if(expr, do: if_block, else: else_block)
  end

  defmacro my_if(expr, do: if_block) do
    Control.do_my_if(expr, do: if_block, else: nil)
  end

  def do_my_if(expr, do: if_block, else: else_block) do
    quote do
      case unquote(expr) do
          result when result in [false, nil] -> unquote(else_block)
          _ -> unquote(if_block)
      end
    end
  end
end

Which then results in:

iex> import Control
Control
iex> my_if(2==2, do: IO.inspect(true), else: IO.inspect(false))
true
true

:slight_smile:

I can detail ‘why’ if curious, but looking over the code and getting it to ‘click’ yourself can often be more enlightening in the tough world of macros. :slight_smile:

Wow i have much to learn, am i right to say that unquote evaluates the expression it is “unquoting” and inserts the result in the expanded AST. Hence my original implementation leads to

Control.do_my_if(true, do: true, else: false)

when running your example usage

unquote does not execute anything.

But, lets try to expanding your macro by hand. Perhaps we can enlighten you that way!

Your intitial version:

defmodule Control do
  defmacro my_if(expr, do: if_block, else: else_block) do
    quote do
      Control.do_my_if(unquote(expr), do: unquote(if_block), else: unquote(else_block))
    end
  end

  defmacro my_if(expr, do: if_block) do
    quote do
      Control.do_my_if(unquote(expr), do: unquote(if_block), else: nil)
    end
  end

  def do_my_if(expr, do: if_block, else: else_block) do
    case (expr) do
        result when result in [false, nil] -> else_block
        _ -> if_block
    end
  end
end

In the wollowing steps, I wont use the AST, but the human readable representation of the AST to simplify things a bit.

And now lets expand Controlö.my_if(false, do: IO.inpsect true, then: IO.inspect false:

First lets fill in the gaps of the Macro:

quote do
  Control.do_my_if(unquote(false, do: unquote(IO.inspect true), then: unquote(IO.inspect false)))
end

Now lets do the unquote:

Control.do_my_if(false, do: IO.inspect true, then: IO.inspect false)

As you can see, we have finished expanding and compiling the macro. We have left a plain function call in the BEAM-byte code. Its arguments will be evaluated at runtime.

Now lets try the version of @OvermindDL1 which I won’t paste again, using the same snippet as above:

The first thing to mention is, that there is no quote in the macro definition, therefore the function is called at compile time! In that function is a quote, therefore wi will look at that now:

quote do
  case unquote(false) do
      result when result in [false, nil] -> unquote(IO.inspect false)
      _ -> unquote(IO.inspect true)
  end
end

And now unquote this:

case false do
    result when result in [false, nil] -> IO.inspect false
    _ -> IO.inspect true
end

As you can see, this version expands into a case-expression and not a function call. Also since do_my_if is called from inside Control during compiletime (not from another module during runtime as in your version), you should be able to even defp it in Overminds version.


Somewhere burried in the Macro module, there was a function or macro which was able to print the expanded sourcecode of a macro-call. That is very nice when debugging them.

2 Likes

Yep, NobbZ said it wonderfully!

Macro.expand_once (or Macro.expand to expand all steps) along with I think it was Macro.to_string or something like that. :slight_smile:

s/unquote// :slight_smile:

That never happened :wink:

1 Like

Thanks guys for helping my out with this. It’s really helpful to know how to expand on the macros by hand. As an extension to this “lesson”, may i verify my understanding using a streamlined version example below:

It will be very very helpful if you can go through again by hand how this whole chain of a more streamlined example got expanded or evaluated. I understand it to be that during “AST expansion phase”, the body of call_go_macro will be expanded into do_go(1) which ultimately expands to the AST of IO.inspect unquote(1). Then during the final “compilation to bytecode” phase that AST is compile to bytecode and whenever call_go_macro is called upon, IO.inspect(1)'s bytecode representation is executed during runtime

defmodule Test do
  defmacro go do
    do_go(1)
  end

  def do_go(var) do
    quote do
      IO.inspect unquote(var) 
    end
  end

  def call_go_macro do
    go()
  end
end