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
  2. Macros — Code that writes code — They allow us

    to influence which code gets evaluated
  3. 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. #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. 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. 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. 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. But then we pass it to our programming language... ...

    and you won't believe what happens next.
  9. 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. 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"
  11. 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
  12. Code is data, data is code — Incredibly powerful —

    Writing code that writes code — The language becomes self-aware...
  13. Code is data — Sounds weird? It is — Sounds

    abstract? It is — Don't get it? That's okay
  14. 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")
  15. 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]
  16. Macros to the rescue defmodule MacroUnless do defmacro unless(clause, block)

    do quote do if !unquote(clause), do: unquote(block) end end end
  17. 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
  18. 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
  19. 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
  20. 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!
  21. 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]]}
  22. 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]}
  23. 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}]]}
  24. 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!"]}]]}
  25. Our macro again defmodule MacroUnless do defmacro unless(clause, block) do

    quote do if !unquote(clause), do: unquote(block) end end end
  26. 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