(Metaprogramming)
# elixir/kernel.ex defmacro defmacro(call, expr \\ nil) do define(:defmacro, call, expr, __CALLER__) end
defmodule QuickMathz do @values [{:one, 1}, {two: two}, {:three, 3}] for {name, value} <- @values do def unquote(name)(), do: unquote(value) end end QuickMathz.three # => 3
html do head do title do text "Hello To Our HTML DSL" end end body do h1 class: "title" do text "Introduction to metaprogramming" end p do text "Metaprogramming with Elixir is really awesome!" end end end
Ecto (Database Access, Data Mapping & Validation):
from o in Order, where: o.created_at > ^Timex.shift(DateTime.utc_now(), days: -2) join: i in OrderItems, on: i.order_id == o.id
Plug (http library web specification)
get "/hello" do send_resp(conn, 200, "world") end match _ do send_resp(conn, 404, "oops") end
Absinthe (GraphQL Server implementation)
field :subscribe, :subscription_plan do arg(:plan_id, non_null(:integer)) arg(:card_token, :string, default_value: nil) arg(:coupon, :string, default_value: nil) middleware(JWTAuth) resolve(&BillingResolver.subscribe/3) end
ExUnit
defmodule Blogit.ComponentTest do use ExUnit.Case, async: true describe "when a module uses it with `use Blogit.Component`" do test "injects a function base_name/0, which returns the name of " <> "the module in underscore case" do assert TestComponent.base_name() == "test_component" end end # ... блогът има повече от 1 тест, вярвайте! end
В други езици в тестовете пишем:
assert true assert_equal 5, 4 assert_operator 5, :< 4
В Elixir можем само така:
assert true assert 5 == 4 assert 5 < 4
Всъщност мета-програмирането в Elixir е толкова силно, че ни позволява:
quote do: 1 + 1 # => {:+, [context: Elixir, import: Kernel], [1, 1]}
quote do: sum(1, 2, 3) # => {:sum, [], [1, 2, 3]}
%{}
quote do: %{1 => 2} # => {:%{}, [], [{1, 2}]}
Малко по-дълъг пример:
quote do html do head do title do text "Hello To Our HTML DSL" end end end end
{:html, [], [ [ do: {:head, [], [ [ do: {:title, [], [ [ do: {:text, [], ["Hello to our HTML DSL"]} ] ]} ] ]} ] ]}
quote do: x # => {:x, [], Elixir}
Видяхме, че са в следния формат:
{<име на функция | tuple>, <контекст>, <списък от аргументи | атом>} # ^ важно # ^ важно # ^ няма да говорим за това
Това са единствените неща, чийто AST е самият израз.
:sum #=> Atoms 1.0 #=> Numbers [1, 2] #=> Lists "strings" #=> Strings {key, value} #=> Tuples with two elements
Идеи защо списъците с 2 елемента са литерали?
iex(1)> quote do: {} {:{}, [], []} iex(2)> quote do: {1} {:{}, [], [1]} iex(3)> quote do: {1,2} {1, 2} iex(4)> quote do: {1,2,3} {:{}, [], [1, 2, 3]}
Какво става с променливите в тези изрази?
iex(3)> x = 1 1 iex(4)> quote do: x + 1 {:+, [context: Elixir, import: Kernel], [{:x, [], Elixir}, 1]} # ^ Защо!?
quote
quote do: unquote(x) + 1 # => {:+, [context: Elixir, import: Kernel], [1, 1]}
unquote
ast1 = quote do: 1 + 2 ast2 = quote do: unquote(ast1) + 3 Macro.to_string(ast2) # => "1 + 2 + 3"
fun = :hello Macro.to_string(quote do: unquote(fun)(:world)) # => "hello(:world)"
[1, 2, 6]
[3, 4, 5]
2
6
inner = [3, 4, 5] Macro.to_string(quote do: [1, 2, unquote(inner), 6]) # => "[1, 2, [3, 4, 5], 6]"
inner = [3, 4, 5] Macro.to_string(quote do: [1, 2, unquote_splicing(inner), 6]) # => "[1, 2, 3, 4, 5, 6]"
kw = [foo: :bar, baz: :quix] Macro.to_string(quote do: %{unquote_splicing(kw)}) # => "%{foo: :bar, baz: :quix}"
defmodule Hello do defmacro say(name) do quote bind_quoted: [name: name] do "Здравей #{name}, как е?" end end end
defmodule Hello do defmacro say(name) do quote do "Здравей #{unquote(name)}, как е?" end end end
iex(1)> Hello.say("Ники") "Здравей Ники, как е?" iex(2)> name ** (CompileError) iex:4: undefined function name/0
Пълен списък с опции на quote
defmacro
defmodule FMI do defmacro if(condition, do: do_clause, else: else_clause) do quote do case unquote(condition) do x when x, [false, nil] -> unquote(else_clause) _ -> unquote(do_clause) end end end end
iex(1)> require FMI # => FMI iex(2)> FMI.if true do ...(2)> 1 ...(2)> else ...(2)> 2 ...(2)> end # => 1
FMI.if(true, [do: 1, else: 2])
def foo(), do: :work
do/end
defmodule T do def(foo(), [do: :work]) end
def/defp/defmacro/defmacrop/defmodule
defmodule(Math, [ {:do, def(add(a, b), [{:do, a + b}])} ])
Пример unless/if
unless/if
ast = quote do FMI.unless true do IO.puts "Hello" end end # {{:., [], [{:__aliases__, [alias: false], [:FMI]}, :unless]}, [], # [ # true, # [ # do: {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], # ["Hello"]} # ] # ]}
След това:
require FMI Macro.expand_once(ast, __ENV__) # {:if, [context: FMI, import: Kernel], # [ # {:!, [context: FMI, import: Kernel], [true]}, # [ # do: {{:., [], # [ # {:__aliases__, [alias: false, counter: -576460752303423390], [:IO]}, # :puts # ]}, [], ["Hello"]}, # else: nil # ] # ]}
__ENV__ е структура от Macro.Env.t, която съдържа информация за текущия контекст - import/require и т.н.
__ENV__
Macro.Env.t
Macro.expand_once
Пример: Adder
Adder
Пример: ExActor
defmodule OurModule do defmacro __using__(opts) do quote do def get_opts(), do: unquote(opts) end end end defmodule SeeUsing do use OurModule, option: "Hello" end SeeUsing.get_opts() # => [option: "Hello"]
defmodule SeeUsing do require OurModule OurModule.__using__(option: "Hello") end SeeUsing.get_opts() # => [option: "Hello"]
Other module callbacks
Пример:
defmodule A do defmacro __before_compile__(_env) do quote do def hello, do: "world" end end end defmodule B do @before_compile A end B.hello() #=> "world"
use GenServer
var!
ast = quote do if a == 42 do "The answer is?" else "Mehhh" end end Code.eval_quoted ast, a: 42 # warning: variable "a" does not exist and is being expanded to "a()", please use parentheses to remove the ambiguity or chang # e the variable name # nofile:1 # # ** (CompileError) nofile:1: undefined function a/0 # (stdlib) lists.erl:1354: :lists.mapfoldl/3 # (elixir) expanding macro: Kernel.if/2 # nofile:1: (file) # # BOOOOOOOOOM
ast = quote do if var!(a) == 42 do "The answer is?" else "Mehhh" end end Code.eval_quoted ast, a: 42 # => {"The answer is?", [a: 42]} Code.eval_quoted ast, a: 1 # => {"Mehhh", [a: 1]}
defmodule Dangerous do defmacro rename(new_name) do quote do var!(name) = unquote(new_name) end end end # => {:module, Dangerous, ..... require Dangerous # => Dangerous name = "Слави" # => "Слави" Dangerous.rename("Вало") # => "Вало" name # => "Вало"
require Dangerous # => Dangerous Dangerous.rename("Вало") # => "Вало" name # => "Вало"
def
quote do: sum all 1, -1