- What is it?
- Examples
- Exercise: Rails Finder Function
Metaprogramming: a programming technique in which programs have the ability to treat programs as their data. In Elixir, this is enabled through macros.
Macro: an Elixir function that receives AST and returns a modified AST.
AST: Abstract Syntax Tree. This tree is code represented as data right before compilation and execution. ASTs are structured like this:
{:function_name, [meta_data_for_context], [argument_list]}- Code that writes code at compile time
- Your code is data
- Define a macro with
defmacro - A macro receives AST and returns AST
- When a macro is used, it will interpret the AST
--
# Normal if statement
if true, do: this(), else: that()
# if statement with a little more clarity
if(true, [do: this(), else: that()])
# if statement represented as an AST fragment
{:if, [context: Elixir, import: Kernel],
[true, [do: {:this, [], []}, else: {:that, [], []}]]}
# Definition of if/2
defmacro if(condition, do: do_block, else: else_block) do
quote do
case unquote(condition) do
result when result in [false, nil] -> unquote(else_block)
_ -> unquote(do_block)
end
end
end- Quoting and unquoting code manages interpreting (or not) code.
- Quote will return as AST
- Unquote will interpret the AST
- Think of it as string interpolation:
my_string = "hey! this is code!"
puts "and this is like a macro that's unquoting: #{my_string}"
#=> "and this is like a macro that's unquoting: hey! this is code!"
puts "opposed to quoting \"my_string\""
#=> "opposed to quoting my_string".blockquote[Elixir already provides mechanisms to write your every day code in] .blockquote[a simple and readable fashion by using its data structures and] .blockquote[functions. Macros should only be used as a last resort. Remember] .blockquote[that explicit is better than implicit. Clear code is better than] .blockquote[concise code.]
- Avoid it.
- data > functions > macros
- When you want a DSL (Rails is a huge DSL)
- Sometimes DSLs are OK
- Don't bring OO to functional
- Execution order
- Binding
- Hygiene
Goal:
MyApp.Repo.get(MyApp.User, 1)
# to
MyApp.User.find(1)defmodule Common.FinderFunctions do
defmacro __using__(opts) do
repo = Keyword.get(opts, :repo)
quote do
import Ecto.Query
# do more stuff
end
end
end
defmodule User do
use Ecto.Schema
use Common.FinderFunctions, repo: MyApp.Repo
# This will require the module, and then call it's __using__ function.
# Common.FinderFunctions.__using__([repo: MyApp.Repo])
# and interpret it within the context of module User
# .. schema, changeset
end# quote do ...
def find(id) do
unquote(repo).get(__MODULE__, id)
# In this case, __MODULE__ will return MyApp.User
end# in IEx
iex(1)> Repo.get(User, 1)
[debug] QUERY OK source="users" db=0.9ms
SELECT u0."id", u0."email", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [1]
%MyApp.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
email: "test@user.com",
id: 1, inserted_at: #Ecto.DateTime<2017-04-23 05:58:02>, password: nil,
updated_at: #Ecto.DateTime<2017-04-23 05:58:02>
}
iex(2)> User.find(1)
[debug] QUERY OK source="users" db=0.9ms
SELECT u0."id", u0."email", u0."inserted_at", u0."updated_at" FROM "users" AS u0 WHERE (u0."id" = $1) [1]
%MyApp.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
email: "test@user.com",
id: 1, inserted_at: #Ecto.DateTime<2017-04-23 05:58:02>, password: nil,
updated_at: #Ecto.DateTime<2017-04-23 05:58:02>
}