Write Less, Do More (and Have Fun!) With
ELIXIR MACROS
@chris_mccord
Erlang Factory SF, 2014
Erlang Factory SF, 2014
Topics
- What are macros?
- Macro use-cases
- Eliminating boilerplate
- Advanced compile time code generation
- Domain Specific Languages (DSLs)
- Fun!
- Writing Macros - A Guided Lesson
- assert macro
- Testing DSL
- Real-world use cases and examples
- Elixir's unicode handling
- Phoenix Web Framework
Macro RULE #1
Don't write macros
MACRO RULE #2
USE MACROS GRATUITOUSLY
What Are macros?
- Code that writes code
- Elixir itself is primarily built with macros
- if, unless, cond
- def, defmodule
- Full access to Elixir at compile time
What Are Macros?
Modules have full access to Elixir at compile time
→
defmodule Calculator do IO.puts "Any code in here is valid!" def add(a, b) do a + b end end IO.puts Calculator.add(1, 2)
$ elixir calculator.exs Any code in here is valid! 3
What ARE MACROS?
- Elixir code can be represented entirely by Elixir's own data structures
iex> quote do: (1 + 2) - 5 * 10
{:-, [],
[{:+, [], [1, 2]},
{:*, [], [5, 10]}]}
quote
- Returns the representation of any expression (AST)
- AST is represented as a series of three element tuples
- The first element is always an atom or another tuple
- The second element represents metadata
- The third element is the arguments for the function call
quotE
iex> quote do: div(10, 2)
{:div, [], [10, 2]}
iex> add = fn a, b -> a + b end
iex> quote do: add.(1, 2)
{
{:., [], [{:add, [], Elixir}]},
[],
[1, 2]
}
ASSERT MACRO
assert MACRO
iex> quote do: 5 == 5 {:==, [...], [5, 5]} iex> quote do: 5 > 0 {:>, [...], [5, 0]}
defmodule Assertions do
defmacro assert({operator, _, [lhs, rhs]}) do
quote do
do_assert unquote(operator),
unquote(lhs),
unquote(rhs)
end
end
...
def do_assert(:==, lhs, rhs) when lhs == rhs do
IO.write(".")
end
def do_assert(:==, lhs, rhs) do
IO.puts """
FAIL: Expected: #{lhs}
to be equal to: #{rhs}
"""
end
defmodule Assertions do
...
def do_assert(:>, lhs, rhs) when lhs > rhs do
IO.write(".")
end
def do_assert(:>, lhs, rhs) do
IO.puts """
FAIL: Expected: #{lhs}
to be greater than: #{rhs}
"""
end
...
end
defmodule MyTest do
import Assertions
def run do
assert 1 == 1
assert 3 > 1
assert 5 == 0
assert 0 > 8
end
end
iex> MyTest.run ..
FAIL: Expected: 5 to be equal to: 0 FAIL: Expected: 0 to be greater than: 8
use
use
is a macro that provides a common API for extension
defmodule MyTest do
use Assertions # invokes __using__ hook
test "adding two numbers" do
assert 5 + 5 == 10
assert 5 + 1 == 6
end
test "zero is greater than 1" do
assert 0 > 1
end
end
iex> MyTest.run
use
$ elixir my_test.exs
.
FAIL: "zero is greater than 1"
Expected: 0
to be greater than: 1
test "zero is greater than 1" do
assert 0 > 1
end
# Assertions Module
defmacro __using__(_opts) do
quote do
import unquote(__MODULE__)
Module.register_attribute __MODULE__, :tests, accumulate: true,
persist: false
@before_compile unquote(__MODULE__)
end
end
...
defmacro test(desc, do: test_block) do
func_name = desc
|> String.downcase
|> String.replace(~r/[^a-zA-Z0-9]/, "_")
|> binary_to_atom
quote do
@tests {unquote(desc), unquote(func_name)}
def unquote(func_name)(), do: unquote(test_block)
end
end
...
defmacro __before_compile__(_env) do
quote do
def run do
@tests |> Enum.reverse |> Enum.each fn {desc, test_case} ->
report desc, apply(__MODULE__, test_case, [])
end
end
end
end
def report(_desc, :ok), do: IO.write "."
def report(desc, {:fail, message}) do
IO.puts """
FAIL: "#{desc}"
#{message}
"""
end
defmodule MyTest do
use Assertions
# @tests { "adding two numbers",
# :adding_two_numbers }
test "adding two numbers" do
assert 5 + 5 == 10
end
# @tests { "zero is greater than 1",
# :zero_is_greater_than_1 }
test "zero is greater than 1" do
assert 0 > 1
end
end
iex> MyTest.run
.
FAIL: "zero is greater than 1"
Expected: 0
to be greater than: 1
unicode SUPPORT
# Ruby
irb> "Thanks José!".upcase
"THANKS JOSé!"
irb> "noël".reverse
"l̈eon"
# Elixir
iex> String.upcase("Thanks José!")
"THANKS JOSÉ!"
iex> String.reverse("noël")
"lëon
Elixir Unicode Support
- Unicode handled by pattern matched functions generated at compile time
- Functions generated entirely from `UnicodeData.txt` codepoint database
- Supporting any additional codepoints in the future requires simply editing UnicodeData.txt and recompiling
UnicodeData.txt
0047;LATIN CAPITAL LETTER G;Lu;0;L;;;;;N;;;;0067;
0048;LATIN CAPITAL LETTER H;Lu;0;L;;;;;N;;;;0068;
0049;LATIN CAPITAL LETTER I;Lu;0;L;;;;;N;;;;0069;
004A;LATIN CAPITAL LETTER J;Lu;0;L;;;;;N;;;;006A;
004B;LATIN CAPITAL LETTER K;Lu;0;L;;;;;N;;;;006B;
...
1F680;ROCKET;So;0;ON;;;;;N;;;;;
1F681;HELICOPTER;So;0;ON;;;;;N;;;;;
1F682;STEAM LOCOMOTIVE;So;0;ON;;;;;N;;;;;
1F683;RAILWAY CAR;So;0;ON;;;;;N;;;;;
1F684;HIGH-SPEED TRAIN;So;0;ON;;;;;N;;;;;
1F686;TRAIN;So;0;ON;;;;;N;;;;;
1F687;METRO;So;0;ON;;;;;N;;;;;
unicode.ex
defmodule String.Unicode do
data_path = Path.join(__DIR__, "UnicodeData.txt")
{codes, whitespace} = Enum.reduce File.stream!(data_path)...
def upcase(string), do: do_upcase(string) |> iolist_to_binary
lc {codepoint, upper,_,_} inlist codes, upper != codepoint do
defp do_upcase(unquote(codepoint) <> rest) do
unquote(:binary.bin_to_list(upper)) ++ do_upcase(rest)
end
end
end
unicode.ex
defp do_upcase("é" <> rest) do
["É"] ++ upcase(rest)
end
defp do_upcase("ë" <> rest) do
["Ë"] ++ upcase(rest)
end
defp do_upcase(🚀 <> rest) do
[🚀] ++ upcase(rest)
end
PHoenix Web Framework
Router DSL
defmodule Router do
use Phoenix.Router, port: 4000
get "/pages/:page", Ctrls.Pages, :show, as: :page
get "/files/*path", Ctrls.Files, :show
post "/files", Ctrls.Files, :upload
resources "users", Ctrls.Users do
resources "comments", Ctrls.Comments
end
end
get "/pages/:page", Ctrls.Pages, :show, as: :page
def match(conn, :get, ["pages", page]) do
conn = conn.params(Dict.merge(conn.params, [
{"page", page}
]))
apply(Ctrls.Pages, :show, [conn])
end
get "/files/*path", Ctrls.Files, :show
def match(conn, :get, ["files" | path]) do
conn = conn.params(Dict.merge(conn.params, [
{"path", Phoenix.Router.Path.join(path)}
]))
apply(Ctrls.Files, :show, [conn])
end
...
defmodule Router do def match(conn, :get, ["pages", page]) def match(conn, :get, ["files" | path]) def match(conn, :post, ["files"]) def match(conn, :get, ["users"]) def match(conn, :get, ["users", id, "edit"]) def match(conn, :get, ["users", id]) def match(conn, :get, ["users", "new"]) def match(conn, :post, ["users"]) def match(conn, :put, ["users", id]) def match(conn, :patch, ["users", id]) def match(conn, :delete, ["users", id]) def match(conn, :get, ["users", user_id, "comments"]) def match(conn, :get, ["users", user_id, "comments", id, "edit"]) def match(conn, :get, ["users", user_id, "comments", id]) def match(conn, :get, ["users", user_id, "comments", "new"]) def match(conn, :post, ["users", user_id, "comments"]) def match(conn, :put, ["users", user_id, "comments", id]) def match(conn, :patch, ["users", user_id, "comments", id]) def match(conn, :delete, ["users", user_id, "comments", id]) end
Router Pattern Matching
Route dispatch handled entirely by VM through pattern matching
resources "users", Ctrls.Users do
resources "comments", Ctrls.Comments
end
GET "myapp.com/users/1/comments/2"
["users", "1", "comments","2"]
def match(conn, :get, ["users", user_id, "comments", id])
get "files/*path", Ctrls.Files, :show
GET "myapp.com/Files/Docs/photo/img.png"
["files", "Docs", "photo", "img.png"]
def match(conn, :get, ["files" | path])
get "/pages/:page", Ctrls.Pages, :show, as: :page
resources "users", Ctrls.Users do
resources "comments", Ctrls.Comments
end
iex> Router.user_comments_path(user_id: 123)
"/users/123/comments"
iex> Router.user_comment_path(id: 1, user_id: 123)
"/users/123/comments/1"
iex> Router.page_path(page: "home")
"/pages/home"
FUN
defmodule Hub do
"https://github.com/chrismccord.json?tab=repositories"
|> HTTPotion.get
|> apply(:body, [])
|> JSON.decode!
|> Enum.each fn result ->
repo = result["repository"]
def unquote(binary_to_atom(repo["name"]))() do
unquote(repo)
end
end
def go(repo) do
System.cmd("open #{apply(__MODULE__, repo, [])["url"]}")
end
end
iex> Hub.go(:phoenix)
launches "https://github.com/phoenixframework"
iex> Hub.phoenix["watchers"]
103
Macros RULES SUMMARY
- Don't write macros
- * Write macros responsibly
- Write macros irresponsibly
- Have fun
@chris_mccord
chris@chrismccord.com
freenode: #elixir-lang
github.com/phoenixframework
Elixir Macros
By chrismccord
Elixir Macros
- 19,645