Elixir - Macros On Erlang

324b2e4d8ae9fcbd7b2983f13481075a?s=47 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

324b2e4d8ae9fcbd7b2983f13481075a?s=128

Thorsten Ball

September 14, 2017
Tweet

Transcript

  1. 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
  2. 9.

    Macros — Code that writes code — They allow us

    to influence which code gets evaluated
  3. 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.
  4. 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; }
  5. 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
  6. 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
  7. 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
  8. 19.

    But then we pass it to our programming language... ...

    and you won't believe what happens next.
  9. 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)
  10. 21.
  11. 22.

    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"
  12. 25.

    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
  13. 26.

    Code is data, data is code — Incredibly powerful —

    Writing code that writes code — The language becomes self-aware...
  14. 29.

    Code is data — Sounds weird? It is — Sounds

    abstract? It is — Don't get it? That's okay
  15. 30.

    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")
  16. 31.
  17. 32.

    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]
  18. 34.

    Macros to the rescue defmodule MacroUnless do defmacro unless(clause, block)

    do quote do if !unquote(clause), do: unquote(block) end end end
  19. 35.

    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
  20. 36.

    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
  21. 37.
  22. 38.

    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
  23. 39.

    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!
  24. 41.

    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]]}
  25. 43.

    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]}
  26. 44.

    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}]]}
  27. 45.

    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!"]}]]}
  28. 46.

    Our macro again defmodule MacroUnless do defmacro unless(clause, block) do

    quote do if !unquote(clause), do: unquote(block) end end end
  29. 48.

    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
  30. 51.