Macros 101

kalil@rootstrap.com

github.com/kaozdl

Kalil de Lima

  • Functional language
  • Functional language
  • Ruby Like Syntax
  • Functional language
  • Ruby Like Syntax
  • Can be compiled or Interpreted
  • Functional language
  • Ruby Like Syntax
  • Can be compiled or interpreted
  • Runs on top of Erlang VM
  • Functional language
  • Ruby Like Syntax
  • Can be compiled or interpreted
  • Runs on top of Erlang VM
  • Fault tolerant
  • Functional language
  • Ruby Like Syntax
  • Can be compiled or interpreted
  • Runs on top of Erlang VM
  • Fault tolerant
  • Designed for parallelism
  • Functional language
  • Ruby Like Syntax
  • Can be compiled or interpreted
  • Runs on top of Erlang VM
  • Fault tolerant
  • Designed for parallelism
  • Safe and performant for parallelism

Elixir crash course

# Comments start with a hash

bar = 42 # Name assignment

# Basic Types

1                # Integers
1.0              # Floats
true             # Booleans
:foo             # Atoms
"bar"            # Strings
[1,2,3]          # Lists
{1,2,3}          # Tuples
%{:var => "foo"} # Maps

Elixir crash course

# Modules contain functions

defmodule MyModule do # Modules are defined by defmodule
  
  @module_constant %{:var => "foo", :foo => 42} # this is constant 
                                                # to the entire module
  def in_module_constant?(value) do
  	val = @module_constant    # Pipe operator passes the element on top
              |> Map.fetch(value) # as first argument to the function below
    
    case val do               # case allows us to pattern match
    	{:ok, _} -> true      # over the result, deciding each case
        :error -> false
    end
  end
    
  defp forty_two?(value) do # this is a private function
    value == 42             # only can be used inside
  end                       # this module.

end

The Simplest elixir usage

Module.function(argument1, argument2)

Most of Elixir is written in Elixir

Most of Elixir is written in Elixir

if 1 + 2 == 3 do
  "this"
else
  "that"
end

Most of Elixir is written in Elixir

if 1 + 2 == 3,
do:
  "this",
else:
  "that"

Most of Elixir is written in Elixir

if 1 + 2 == 3, do: "this", else: "that"

Most of Elixir is written in Elixir

if 1 + 2 == 3, do: "this", else: "that"
if(1 + 2 == 3, do: "this", else: "that")

Most of Elixir is written in Elixir

if 1 + 2 == 3, do: "this", else: "that"
if(1 + 2 == 3, do: "this", else: "that")
if(1 + 2 == 3, [do: "this", else: "that"])

Most of Elixir is written in Elixir

if 1 + 2 == 3, do: "this", else: "that"
if(1 + 2 == 3, do: "this", else: "that")
if(1 + 2 == 3, [do: "this", else: "that"])
if((1 + 2) == 3, [do: "this", else: "that"])

Most of Elixir is written in Elixir

if 1 + 2 == 3, do: "this", else: "that"
if(1 + 2 == 3, do: "this", else: "that")
if(1 + 2 == 3, [do: "this", else: "that"])
if((1 + 2) == 3, [do: "this", else: "that"])
if(Kernel.==(Kernel.+(1, 2), 3), [do: "this", else: "that"])

Most of Elixir is written in Elixir

if 1 + 2 == 3, do: "this", else: "that"
if(1 + 2 == 3, do: "this", else: "that")
if(1 + 2 == 3, [do: "this", else: "that"])
if((1 + 2) == 3, [do: "this", else: "that"])
if(Kernel.==(Kernel.+(1, 2), 3), [do: "this", else: "that"])
Kernel.if(Kernel.==(Kernel.+(1, 2), 3), [do: "this", else: "that"])

How does Elixir processes our code?

  • Read code

How does Elixir processes our code?

  • Read code
  • Convert code to AST

How does Elixir processes our code?

  • Read code
  • Convert code to AST
  • Pass the AST to VM

The Elixir Abstract Syntax Tree

Elixir AST elements

# Quote literals

:kalil # Atoms
123456 # Integers
1.2345 # Floats
"foov" # Strings
[1,23] # Lists
{1, 2} # Two element tuples

Elixir AST elements

# 3 element tuples

{        # AST for a variable
  :var,  # Variable name
  [],    # Metadata
  Elixir # Variable context
}

{                                    # AST for a function call
  :+,                                # Function name
  [context: Elixir, import: Kernel], # Metadata
  [1, 2]                             # Function arguments
}

How do we convert our code to AST?

Quote

>> quote do: 1 + 2

{:+, [context: Elixir, import: Kernel], [1, 2]}

Quote

>> quote do: 1 + 2

{:+, [context: Elixir, import: Kernel], [1, 2]}

Unquote

>> bar = 2
>> quote do: 1 + bar

{:+, [context: Elixir, import: Kernel], [1, {:bar, [], Elixir}]}

# We need to unquote the value of bar

>> quote do: 1 + unquote(bar)
{:+, [context: Elixir, import: Kernel], [1, 2]}

Let's put all this in practice!

Lets quote our if

>> quote do: if 1 + 2 == 3, do: "this", else: "that"

{
  :if,           # <-- the call to the if function 
  [...],         # <-- let's ignore metadata
  [
    {
      :==,       # <-- the call to ==
      [...],
      [ 
        {        # <-- the first argument of == is a call to +
          :+,
          [...],
          [1, 2] # <-- the arguments of + are 1 and 2
        },
        3        # <-- the second argument to == is 3
      ]
    },
    [do: "this", else: "that"] # <-- the second argument is a keyword list
  ]
}

Let's Implement our own macros now

Wrapping up

Macros can help us in

  • Boilerplate reduction

Macros can help us in

  • Boilerplate reduction
  • Domain Specific Languages

We can use macros to:

We can use macros to:

  • Reduce boilerplate

We can use macros to:

  • Reduce boilerplate
  • Enforce best practices

We can use macros to:

  • Reduce boilerplate
  • Enforce best practices
  • Improve reliability

We can use macros to:

  • Reduce boilerplate
  • Enforce best practices
  • Improve reliability

While keeping code:

We can use macros to:

  • Reduce boilerplate
  • Enforce best practices
  • Improve reliability

While keeping code:

  • Readable

We can use macros to:

  • Reduce boilerplate
  • Enforce best practices
  • Improve reliability

While keeping code:

  • Readable
  • Maintainable

We can use macros to:

  • Reduce boilerplate
  • Enforce best practices
  • Improve reliability

While keeping code:

  • Readable
  • Maintainable
  • Idiomatic

"The first rule of macro club is:
Don't write macros"

- Stuart Halloway

Bad macros lead to code:

Bad macros lead to code:

  • Unreadable

Bad macros lead to code:

  • Unreadable
  • Unmaintainable

Bad macros lead to code:

  • Unreadable
  • Unmaintainable
  • Unidiomatic

"Code is read many more times than it is written, wich means that the ultimate cost of code is in its reading"

- Sandi Metz

Bonus 🙃

We can use the same idea in Ruby!

# Suppose that we have the following rails class

class Movie < ActiveRecord::Base
  has_many :reviews
  has_many :actors
end

# Classes are executable code
# When they instantiate they will execute
class Movie
  def self.has_many(name)
    puts "#{self} has many #{name}"
    define_method(name) do # We define the method 
      ...                  # reviews on the fly
    end
  end

  has_many :reviews # Here has_many will be called
  has_many :actors  # defining a rewiews method first
                    # and an actors method after that
end

Questions?

Thanks for listening!

Macros 101

By Kalil De Lima

Macros 101

  • 243