Pro Yearly is on sale from $80 to $50! »

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. "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. "Erlang plus a powerful macro system."

  3. Elixir - Macros On Erlang

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

    like programming languages.
  5. I wrote a book! Get it: interpreterbook.com Coupon code for

    20% off: elixirfromthebembel
  6. interpreterbook.com/lost

  7. Disclaimer! Use in production at your own risk

  8. What's a macro?

  9. Macros — Code that writes code — They allow us

    to influence which code gets evaluated
  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.
  11. Two Camps Of Macro Systems — Text-Substitution Macros — Syntactic

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

    — Code as Data Macros
  13. Text-Substitution Macros — The simplest type of macro system —

    Example: The C Pre Processor (cpp)
  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; }
  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
  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
  17. Code is data?

  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
  19. But then we pass it to our programming language... ...

    and you won't believe what happens next.
  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)
  21. None
  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"
  23. Pretty handy, huh?

  24. In language where "code is data" you can do that

    in the language itself!
  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
  26. Code is data, data is code — Incredibly powerful —

    Writing code that writes code — The language becomes self-aware...
  27. Recursion, baby!

  28. (! Lisp (baby))

  29. Code is data — Sounds weird? It is — Sounds

    abstract? It is — Don't get it? That's okay
  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")
  31. Unless - The Naiive Version defmodule FunctionUnless do def unless(clause,

    do: block) do if !clause, do: block end end
  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]
  33. Uh oh iex(3)> FunctionUnless.unless 5 == 5, do: IO.puts("yay condition

    true!") yay condition true! nil
  34. Macros to the rescue defmodule MacroUnless do defmacro unless(clause, block)

    do quote do if !unquote(clause), do: unquote(block) end end end
  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
  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
  37. None
  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
  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!
  40. Return the code you want to see in the world!

    — quote — unquote
  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]]}
  42. unquote — The trusty helper of quote — Punches a

    hole into quote
  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]}
  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}]]}
  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!"]}]]}
  46. Our macro again defmodule MacroUnless do defmacro unless(clause, block) do

    quote do if !unquote(clause), do: unquote(block) end end end
  47. Boom! Macros! Code is data!

  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
  49. Macros let you cross the boundary between language creator and

    language user
  50. * Use sparingly

  51. Thank you

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

    elixirfromthebembel