Skip to content

Instantly share code, notes, and snippets.

@dbernheisel
Created November 30, 2017 23:13
Show Gist options
  • Select an option

  • Save dbernheisel/1ff32f8f9e29c9751904e1593c6e2a1b to your computer and use it in GitHub Desktop.

Select an option

Save dbernheisel/1ff32f8f9e29c9751904e1593c6e2a1b to your computer and use it in GitHub Desktop.
Markdown presentation of Elixir Macros

Elixir Metaprogramming

An introduction

@bernheisel

@thoughtbot


Plan

  1. What is it?
  2. Examples
  3. Exercise: Rails Finder Function

Define

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]}

What is it?

  • 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

Quote/Unquote

  • 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"

When should I use it?

.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

Not talking about:

  • Execution order
  • Binding
  • Hygiene

Let's practice!

Goal:

MyApp.Repo.get(MyApp.User, 1)
# to
MyApp.User.find(1)

Import a module containing the macro

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

Define .find function within macro

# 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>
 }

??? Can someone see a problem? find([1,2,3]) will fail.

Resources:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment