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"} # MapsElixir 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"
endMost 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 tuplesElixir 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