Running at Compile-time
& (Re)Compiling at Runtime
By: Wiebe-Marten Wijnja/Qqwy
Metaprogramming:
Automating creation & changing of a program
Inspiration
Code: Educational "fits on slide" examples
Links to resources with more detail
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
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
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
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
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
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
1 - 3 * 5 + 2
1 - 3 * 5 + 2
iex> quote do 1 - 3 * 5 + 2 end
{:+, [],
[
{:-, [], [1, {:*, [], [3, 5]}]},
2
]}
assert(1 + 2 + 3 > 10)
Comparison with > failed
lhs: 6
rhs: 10
code: 1 + 2 + 3 > 10
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!
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)
from p in Post,
where: p.category == "fresh and new",
order_by: [desc: p.published_at],
select: struct(p, [:id, :title, :body])
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
Running at compile-time
Elixir & Erlang
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!"}
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
Use `:persistent_term.get/put`
*The current version uses ETS. A version based on :persistent_term is in the works:
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
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
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
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
(Re)Compiling at Runtime
Book: 'Metaprogramming Elixir' by Chris McCord https://pragprog.com/book/cmelixir/metaprogramming-elixir