Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Elixir - Macros On Erlang

Thorsten Ball
September 14, 2017

Elixir - Macros On Erlang

An introduction to Elixir's macro system and an investigation into why it's probably the most powerful feature Elixir has.

I gave this talk at the Elixir RheinMain Meetup on 14. September 2017

Thorsten Ball

September 14, 2017
Tweet

More Decks by Thorsten Ball

Other Decks in Programming

Transcript

  1. "Elixir took the Erlang virtual
    machine, BEAM, and put a
    sensible face on it. It gives
    you all the power of Erlang
    plus a powerful macro system."
    — Dave Thomas, author of Programming Elixir, co-author
    of Pragmatic Programmer

    View full-size slide

  2. "Erlang plus a powerful
    macro system."

    View full-size slide

  3. Elixir - Macros On Erlang

    View full-size slide

  4. Who?!
    Thorsten Ball
    Software Developer
    thorstenball.com
    mrnugget / @thorstenball
    I like programming languages.

    View full-size slide

  5. I wrote a book!
    Get it: interpreterbook.com
    Coupon code for 20% off:
    elixirfromthebembel

    View full-size slide

  6. interpreterbook.com/lost

    View full-size slide

  7. Disclaimer!
    Use in production at your own
    risk

    View full-size slide

  8. What's a macro?

    View full-size slide

  9. Macros
    — Code that writes code
    — They allow us to influence which code gets
    evaluated

    View full-size slide

  10. So, meta-programming?
    — Short answer: It depends
    — Long answer: Iiiiitt deeeepeeeeeends
    — Best answer: Eeeh, maybe, depends on who you ask
    — Not the Ruby kind of meta-programming
    — Macros really work with code: not with objects/
    classes/methods/functions/etc.

    View full-size slide

  11. Two Camps Of Macro Systems
    — Text-Substitution Macros
    — Syntactic Macros

    View full-size slide

  12. Two Camps Of Macro Systems
    — Search and Replace Macros
    — Code as Data Macros

    View full-size slide

  13. Text-Substitution Macros
    — The simplest type of macro system
    — Example: The C Pre Processor (cpp)

    View full-size slide

  14. #define GREETING "Hello there"
    int main(int argc, char *argv[])
    {
    #ifdef DEBUG
    printf(GREETING " Debug-Mode!\n");
    #else
    printf(GREETING " Production-Mode!\n");
    #endif
    return 0;
    }

    View full-size slide

  15. Text-Substitution Macros
    — You can go a long way with it
    — But you have to be careful: escaping, scoping,
    overwriting, ...
    — Much more like a templating system than a macro
    system of the second kind

    View full-size slide

  16. Syntactic Macros
    — In the land where "Code Is Data"
    — Yeah, really, it is weird. It needs a small push
    to wrap one's head around it

    View full-size slide

  17. Code is data?

    View full-size slide

  18. Code starts out as text
    — We write code in our text editor
    — We look at text diffs on GitHub
    — We store text in our version control system
    — We modify source code with search/replace

    View full-size slide

  19. But then we pass it to our programming language...
    ... and you won't believe what happens next.

    View full-size slide

  20. Parsers turn code into data!
    — Parsing: turning strings into data structures
    — Just like a JSON parser turns a string into
    objects, arrays, integers, strings, ...
    — Parsers turn code strings into syntax trees (or
    Abstract Syntax Trees)

    View full-size slide

  21. Why?
    — Analyze it
    — "is this valid syntax?"
    — "has this identifier been defined before?"
    — "is this code unused?"
    — Modify it
    — "Let's remove the commented-out code"
    — "Let's replace calls to this function with the body of the function
    itself"
    — Pass it around
    — "lets hand this and the code from the other files to the pretty printer"

    View full-size slide

  22. Pretty handy, huh?

    View full-size slide

  23. In language where "code is
    data" you can do that in the
    language itself!

    View full-size slide

  24. Allow me to explain...
    — Ruby
    — Implemented in C
    — Parse and modify Ruby in C
    — Parser in C, AST in C, code that uses AST in C
    — Elixir
    — "Code is data"
    — Implemented in Erlang (doesn't matter)
    — You can parse and modify Elixir in Elixir
    — Parse in Elixir, AST in Elixir, code that uses AST in Elixir

    View full-size slide

  25. Code is data, data is code
    — Incredibly powerful
    — Writing code that writes code
    — The language becomes self-aware...

    View full-size slide

  26. Recursion, baby!

    View full-size slide

  27. (! Lisp (baby))

    View full-size slide

  28. Code is data
    — Sounds weird? It is
    — Sounds abstract? It is
    — Don't get it? That's okay

    View full-size slide

  29. Let's write a macro in Elixir
    The unless macro.
    The example for macros - in any language!
    Should look like this:
    unless 5 == 3, do: IO.inspect("will be printed")
    unless 3 == 3, do: IO.inspect("will not be printed")

    View full-size slide

  30. Unless - The Naiive Version
    defmodule FunctionUnless do
    def unless(clause, do: block) do
    if !clause, do: block
    end
    end

    View full-size slide

  31. Looking good there...
    iex(1)> c "unless.exs"
    [FunctionUnless]
    iex(2)> FunctionUnless.unless 5 == 3, do: IO.puts("yay condition false")
    yay condition false
    [do: :ok]

    View full-size slide

  32. Uh oh
    iex(3)> FunctionUnless.unless 5 == 5, do: IO.puts("yay condition true!")
    yay condition true!
    nil

    View full-size slide

  33. Macros to the rescue
    defmodule MacroUnless do
    defmacro unless(clause, block) do
    quote do
    if !unquote(clause), do: unquote(block)
    end
    end
    end

    View full-size slide

  34. Just what the alchemist ordered
    iex(1)> c "unless.exs"
    [FunctionUnless, MacroUnless]
    iex(2)> require MacroUnless
    MacroUnless
    iex(3)> MacroUnless.unless 5 == 3, do: IO.puts("condition false")
    condition false
    :ok
    iex(4)> MacroUnless.unless 5 == 5, do: IO.puts("condition true")
    nil

    View full-size slide

  35. How?
    — Macros are evaluated in the "Macro Expansion
    Phase"
    — Macro Expansion: replace calls to macros in the
    source code with the result of the call
    — Parsing -> Macro Expansion Phase -> Compilation/
    Interpretation
    — Macros receive AST nodes and return AST nodes

    View full-size slide

  36. Inspecting a macro
    defmodule MacroUnless do
    defmacro unless(clause, do: block) do
    IO.inspect(clause)
    IO.inspect(block)
    quote do
    if !unquote(clause), do: unquote(block)
    end
    end
    end

    View full-size slide

  37. Inspecting a macro
    iex(9)> MacroUnless.unless 5 == 5, do: IO.puts("condition true")
    {:==, [line: 9], [5, 5]}
    {{:., [line: 9], [{:__aliases__, [counter: 0, line: 9], [:IO]}, :puts]},
    [line: 9], ["condition true"]}
    nil
    Tuples, keyword lists, lists - code is data!

    View full-size slide

  38. Return the code you want to see in the world!
    — quote
    — unquote

    View full-size slide

  39. quote
    iex> quote do: 1 + 2
    {:+, [context: Elixir, import: Kernel], [1, 2]}
    iex> quote do: IO.puts("foobar")
    {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["foobar"]}
    iex> quote do: MyModule.add_two(2)
    {{:., [], [{:__aliases__, [alias: false], [:MyModule]}, :add_two]}, [], [2]}
    iex> quote do: [1, 2, 3, 4]
    [1, 2, 3, 4]
    iex> quote do: [head | tail] = [1, 2, 3, 4]
    {:=, [], [[{:|, [], [{:head, [], Elixir}, {:tail, [], Elixir}]}], [1, 2, 3, 4]]}

    View full-size slide

  40. unquote
    — The trusty helper of quote
    — Punches a hole into quote

    View full-size slide

  41. unquote
    iex> quote do: 1 + 2 + 3
    {:+, [context: Elixir, import: Kernel],
    [{:+, [context: Elixir, import: Kernel], [1, 2]}, 3]}
    iex> quote do: 1 + unquote(2 + 3)
    {:+, [context: Elixir, import: Kernel], [1, 5]}

    View full-size slide

  42. We need unquote
    iex(18)> quote do: if !(clause), do: block
    {:if, [context: Elixir, import: Kernel],
    [{:!, [context: Elixir, import: Kernel], [{:clause, [], Elixir}]},
    [do: {:block, [], Elixir}]]}

    View full-size slide

  43. Unquote to access arguments
    iex(19)> clause = quote do: 5 == 5
    {:==, [context: Elixir, import: Kernel], [5, 5]}
    iex(20)> block = quote do: IO.puts("yay!")
    {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["yay!"]}
    iex(21)> quote do: if !(unquote(clause)), do: unquote(block)
    {:if, [context: Elixir, import: Kernel],
    [{:!, [context: Elixir, import: Kernel],
    [{:==, [context: Elixir, import: Kernel], [5, 5]}]},
    [do: {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [],
    ["yay!"]}]]}

    View full-size slide

  44. Our macro again
    defmodule MacroUnless do
    defmacro unless(clause, block) do
    quote do
    if !unquote(clause), do: unquote(block)
    end
    end
    end

    View full-size slide

  45. Boom! Macros!
    Code is data!

    View full-size slide

  46. Elixir - Macros On Top Of Erlang
    — Why "Erlang" and "a macro system"?
    — Because it's an incredibly powerful macro system
    — Elixir itself is built using this macro system

    View full-size slide

  47. Macros let you cross the
    boundary between language
    creator and language user

    View full-size slide

  48. * Use sparingly

    View full-size slide

  49. Writing An Interpreter In Go
    Coupon code for 20% off:
    elixirfromthebembel

    View full-size slide