Going Meta with Elixir
Running at Compile-time
& (Re)Compiling at Runtime
By: Wiebe-Marten Wijnja/Qqwy
Metaprogramming:
Automating creation & changing of a program
To 'Go Meta'?
Talk Structure & Goal
- What & How to use
- Why useful?
- Examples
Inspiration
Code: Educational "fits on slide" examples
Links to resources with more detail
Running at Compile-time
(Re)compiling at Runtime
Running at Compile-time
Dynamic Compilation
Running at compile-time
Dynamic Compilation
Running at compile-time
defmodule Fibonacci do
@doc """
Returns the n-th fibonacci number.
Slow implementation for educational purposes.
"""
def fib(0) do
0
end
def fib(1) do
1
end
def fib(n) when n > 0 do
fib(n - 1) + fib(n - 2)
end
end
Dynamic Compilation
Running at compile-time
defmodule Example do
if function_exported?(:erlang, :is_map_key, 2) do
def my_fancy_function(param1, param2) do
# ...
end
else
raise(CompileError,
description: "This library requires an Erlang/OTP version that has the `is_map_key` function!"
)
end
end
Dynamic Compilation
Running at compile-time
defmodule Example do
if function_exported?(:erlang, :is_map_key, 2) do
def my_fancy_function(param1, param2) do
# ...
end
else
raise(CompileError,
description: "This library requires an Erlang/OTP version that has the `is_map_key` function!"
)
end
end
$ iex -S mix
Erlang/OTP 22 [erts-10.4.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [hipe]
Compiling 1 file (.ex)
== Compilation error in file lib/compile_error_example.ex ==
** (CompileError) This library requires an Erlang/OTP version that has the `is_map_key` function!
lib/compile_error_example.ex:8: (module)
(stdlib) erl_eval.erl:680: :erl_eval.do_apply/6
Dynamic Compilation
Running at compile-time
defmodule Fibonacci do
@doc """
Returns the n-th fibonacci number.
Slow implementation for educational purposes.
"""
def fib(0) do
0
end
def fib(1) do
1
end
def fib(n) when n > 0 do
fib(n - 1) + fib(n - 2)
end
end
Dynamic Compilation
Running at compile-time
defmodule Fibonacci do
@doc """
Returns the n-th fibonacci number.
Slow implementation for educational purposes.
"""
@known_answers [0, 1, 1, 2, 3, 5, 8, 13, 25]
for {answer, index} <- Enum.with_index(@known_answers) do
def fib(unquote(index)) do
unquote(answer)
end
end
def fib(n) when n > 0 do
fib(n - 1) + fib(n - 2)
end
end
Dynamic Compilation
Running at compile-time
defmodule Fibonacci do
@doc """
Returns the n-th fibonacci number.
Slow implementation for educational purposes.
"""
def fib(0) do 0 end
def fib(1) do 1 end
def fib(2) do 1 end
def fib(3) do 2 end
def fib(4) do 5 end
def fib(5) do 8 end
def fib(6) do 13 end
def fib(7) do 25 end
def fib(n) when n > 0 do
fib(n - 1) + fib(n - 2)
end
end
defmodule String.Unicode.Example do
@moduledoc """
Paraphrased from Elixir's unicode-handling modules.
(See https://github.com/elixir-lang/elixir/tree/master/lib/elixir/unicode)
"""
ranges =
Path.join(__DIR__, "UnicodeData.txt")
|> File.read!()
|> String.split("\n", trim: true)
|> Enum.map(&parse_unicode_range/1)
def unicode_upper?(char)
for {first, last} <- ranges, uppercase_range?(range) do
def unicode_upper?(char) when char in unquote(range) do
true
end
end
def unicode_upper?(char) do
false
end
# ...
end
Ex: Unicode Support
Running at compile-time
Macros
Running at compile-time
- Functions that run at compile-time
- AST Transformations: Code -> Code
- Hygienic (by default): No name clashes
Aside: Abstract Syntax Trees
1 - 3 * 5 + 2
Aside: Abstract Syntax Trees
1 - 3 * 5 + 2
iex> quote do 1 - 3 * 5 + 2 end
{:+, [],
[
{:-, [], [1, {:*, [], [3, 5]}]},
2
]}
Ex: ExUnit's assert
Running at compile-time
assert(1 + 2 + 3 > 10)
Comparison with > failed
lhs: 6
rhs: 10
code: 1 + 2 + 3 > 10
Ex: ExUnit's assert
Running at compile-time
assert(1 + 2 + 3 > 10)
Comparison with > failed
lhs: 6
rhs: 10
code: 1 + 2 + 3 > 10
assert_gt(left, right)
assert_lt(left, right)
assert_eq(left, right)
# ...
# Horrible!
Running at compile-time
defmodule AssertExample do
defmacro assert(expr = {:>, _meta, [lhs, rhs]}) do
quote do
left = unquote(lhs)
right = unquote(rhs)
if left > right do
true
else
IO.puts """
Comparison with > failed
left: #{left}
right: #{right}
code: #{unquote(Code.to_string(expr))}
"""
false
end
end
end
defmacro assert(expr = {:<, _meta, [lhs, rhs]}), do: #...
defmacro assert(expr = {:>=, _meta, [lhs, rhs]}), do: #...
defmacro assert(expr = {:<=, _meta, [lhs, rhs]}), do: #...
defmacro assert(expr = {:!=, _meta, [lhs, rhs]}), do: #...
# ...
defmacro assert(other) do
raise(Assert.CompileError, "Unsupported assert expression #{Macro.to_string(expr)}")
end
end
assert(1 + 2 + 3 > 10)
Ex: ExUnit's assert
Ex: Ecto Queries
Running at compile-time
from p in Post,
where: p.category == "fresh and new",
order_by: [desc: p.published_at],
select: struct(p, [:id, :title, :body])
Ex: Phoenix HTTP Routing
defmodule AppRouter do
use HelloWeb, :router
get "/", PageController, :index
get "/users", UsersController, :index
get "/users/:id", UsersController, :show
get "/users/:id/edit", UsersController, :edit
patch "/users/:id", UsersController, :update
end
- Safe interpolation of values
- Compile-time errors on malformed inputs
- Composition
- Efficient dispatch
- Autogenerated helper functions
Tools
Running at compile-time
- Dynamic Compilation
- Reduce repetitive code
- Conditional compilation
- Definitions based on external sources
- Compile-time Safety
- Macros
- Domain Specific Languages
- Syntactic Sugar
- Code Transformations
- Compile-time Safety
- Domain Specific Languages
(Re)Compiling at runtime
Elixir & Erlang
Hot-Code Reloading
(Re)Compiling at runtime
Hot-Code Reloading
(Re)Compiling at runtime
- Deploy new version of app
- while it is running
- without dropping any data, sockets etc!
Hot-Code Reloading
(Re)Compiling at runtime
iex> user = MyRepo.get(User, 42)
%User{name: "W-M", id: 42}
iex> MyModule.do_something_with(user)
** (ArgumentError) some problem
# change the module
# now, reload the module
iex> r MyModule
iex> MyModule.do_something_with(user)
{:ok, "Something!"}
(Re)Compiling at runtime
- Improved efficiency for write-seldom, read-often data
- "Efficiency hack": (Ab)uses module constant pool
Ex: FastGlobal & :persistent_term
Ex: FastGlobal & :persistent_term
(Re)Compiling at runtime
- Efficiency for write-seldom, read-often data
defmodule ExampleGlobal do
def get(key, default \\ nil) when is_atom(key) do
if function_exported?(module_name(key), :__val__, 0) do
module_name(key).__val__()
else
default
end
end
def put(key, val) when is_atom(key) do
Code.eval_quoted(
quote bind_quoted: [module_name: module_name(key), val: val] do
:code.purge(module_name)
:code.delete(module_name)
defmodule module_name do
def __val__, do: unquote(val)
end
end)
:ok
end
defp module_name(key), do: :"ExampleGlobal.#{key}"
end
(Re)Compiling at runtime
- Don't use it unless you need to (try Agents, ETS, etc. first)
- Benchmark!
-
Use `:persistent_term.get/put`
Ex: FastGlobal & :persistent_term
Ex: TZData
(Re)Compiling at runtime
Ex: TZData
(Re)Compiling at runtime
- Checks remote 'timezone database' once a day
- If there are changes, recompile*
*The current version uses ETS. A version based on :persistent_term is in the works:
Ex: Introspection
(Re)Compiling at runtime
iex>
defmodule Fibonacci do
@doc """
Returns the n-th fibonacci number.
Slow implementation for educational purposes.
"""
def fib(0) do
0
end
def fib(1) do
1
end
def fib(n) when n > 0 do
fib(n - 1) + fib(n - 2)
end
end
Ex: Introspection
(Re)Compiling at runtime
iex> Rexbug.start("Fibonacci.fib(1)")
:ok
defmodule Fibonacci do
@doc """
Returns the n-th fibonacci number.
Slow implementation for educational purposes.
"""
def fib(0) do
0
end
def fib(1) do
send_trace_information()
1
end
def fib(n) when n > 0 do
fib(n - 1) + fib(n - 2)
end
end
Ex: Introspection
(Re)Compiling at runtime
iex> Rexbug.start("Fibonacci.fib(1)")
:ok
iex> Fibonacci.fib(4)
# 23:28:41 #PID<0.204.0> IEx.Evaluator.init/4
# Fibonacci.fib(1)
# 23:28:41 #PID<0.204.0> IEx.Evaluator.init/4
# Fibonacci.fib(1)
# 23:28:41 #PID<0.204.0> IEx.Evaluator.init/4
# Fibonacci.fib(1)
defmodule Fibonacci do
@doc """
Returns the n-th fibonacci number.
Slow implementation for educational purposes.
"""
def fib(0) do
0
end
def fib(1) do
send_trace_information()
1
end
def fib(n) when n > 0 do
fib(n - 1) + fib(n - 2)
end
end
Ex: Introspection
(Re)Compiling at runtime
iex> Rexbug.start("Fibonacci.fib(1)")
:ok
iex> Fibonacci.fib(4)
# 23:28:41 #PID<0.204.0> IEx.Evaluator.init/4
# Fibonacci.fib(1)
# 23:28:41 #PID<0.204.0> IEx.Evaluator.init/4
# Fibonacci.fib(1)
# 23:28:41 #PID<0.204.0> IEx.Evaluator.init/4
# Fibonacci.fib(1)
iex> Rexbug.stop
:stopped
redbug done, local_done - 5
defmodule Fibonacci do
@doc """
Returns the n-th fibonacci number.
Slow implementation for educational purposes.
"""
def fib(0) do
0
end
def fib(1) do
1
end
def fib(n) when n > 0 do
fib(n - 1) + fib(n - 2)
end
end
Ex: Introspection
(Re)Compiling at runtime
- :erlang.trace, Redbug, Rexbug
- Runs in your live app
- Patch introspection calls into functions matching pattern only!
- No slowdown for rest of app
Tools
(Re)Compiling at Runtime
- Recompilation at Runtime
- Hot code deployments
- React to things that change independently from your app release schedule
- Persisting terms for fast global read access.
- Introspection
Running at Compile-time
(Re)compiling at Runtime
With Great Power...
Resources & Further Reading
-
Dynamic Compilation
- Elixir's Unicode handling implementation https://github.com/elixir-lang/elixir/tree/master/lib/elixir/unicode
-
Macros
- "Understanding Macros" by Saša Jurić https://www.theerlangelist.com/article/macros_1
- Ecto queries https://hexdocs.pm/ecto/Ecto.Query.html
- Phoenix HTTP Routing https://hexdocs.pm/phoenix/Phoenix.Router.html
-
Hot Code Reloading
- Erlang - The Movie https://www.youtube.com/watch?v=BXmOlCy0oBM
- "Hot Code Reloading in Elixir" https://blog.appsignal.com/2018/10/16/elixir-alchemy-hot-code-reloading-in-elixir.html
- TZData https://github.com/lau/tzdata
- FastGlobal https://github.com/discordapp/fastglobal
- :persistent_term http://erlang.org/doc/man/persistent_term.html
-
Introspection
- Rexbug https://github.com/nietaki/rexbug
- :erlang.trace http://erlang.org/doc/man/erlang.html#trace-3
Book: 'Metaprogramming Elixir' by Chris McCord https://pragprog.com/book/cmelixir/metaprogramming-elixir
Thank you!
Questions?
Going Meta with Elixir's
By qqwy
Going Meta with Elixir's
- 834