Using Macros to Extend Elixir - Introducing Elixir: Getting Started in Functional Programming (2014)

Introducing Elixir: Getting Started in Functional Programming (2014)

Chapter 13. Using Macros to Extend Elixir

You have now learned enough Elixir to write interesting and fairly powerful programs. Sometimes, though, you need to extend the language itself in order to make your code easier to read or to implement some new functionality. Elixir’s macro feature lets you do this.

Functions versus Macros

On the surface, macros look a lot like functions, except that they begin with defmacro instead of def. However, macros work very differently than functions. The best way to explain the difference is to show you Example 13-1, which is in directory ch13/ex1-difference.

Example 13-1. Showing the difference between function and macro calls

defmodule Difference do

defmacro m_test(x) do

IO.puts("#{inspect(x)}")

x

end

def f_test(x) do

IO.puts("#{inspect(x)}")

x

end

end

In order to use a macro, you must require the module that it’s in. Type the following in the shell:

iex(1)> c("difference.ex")

[Difference]

iex(2)> require Difference

[]

iex(3)> Difference.f_test(1 + 3)

4

4

iex(4)> Difference.m_test(1 + 3)

{:+,[line: 4],[1,3]}

4

Line 3 gives you exactly what you’d expect — Elixir evaluates 1 + 3 and passes it on to the f_test function, which prints the number 4 and returns the number 4 as its result.

Line 4 may be something of a surprise. Instead of an evaluated expression, the argument is a tuple that is the internal representation of the code before it is executed. The macro returns the tuple (in Elixir terms, the macro has been expanded) and then that tuple is passed on to Elixir to be evaluated.

NOTE

The first item in the tuple is the operator, the second item is a list of metadata about the operation, and the third item is a list of the operands.

A Simple Macro

Because defmacro gets the code before Elixir has had a chance to evaluate it, a macro has the power to transform the code before sending it on to Elixir for evaluation. Example 13-2 is a macro that creates code to double whatever its argument is. (This is something that could much more easily be accomplished with a function, but I need to start with something easy.) It works by manually creating the tuple that Elixir will recognize as a multiplication operation. You can find it in ch13/ex2-double.

Example 13-2. A manually-created macro

defmodule Double do

defmacro double x do

{:*, [], [2, x]}

end

end

iex(1)> c("double.ex")

[Double]

iex(2)> require Double

[]

iex(3)> Double.double(3)

6

iex(4)> Double.double(3 * 7)

42

That works, but there must be an easier way. It would be nice if you could say, “turn this Elixir code into internal format” so that Elixir creates the tuples for you. In fact, you can do this by using quote, which takes any Elixir expression and converts it to internal format:

iex(5)> quote do: 1 + 3

{:+,[import: Kernel],[1,3]}

iex(6)> x = 20

20

iex(7)> quote do: 3 * x + 20

{:+,[import: Kernel],[{:*,[import: Kernel],[3,{:x,[],Elixir}]},20]}

As you see, quote takes normal Elixir code and converts it to the internal format that macros accept as input and return as their expanded result. You might be tempted to rewrite the macro from the previous example as follows:

defmodule Double do

defmacro double(x) do

quotedo: 2 * x

end

end

If you try this code, it won’t work. The reason is that you are saying, “turn 2 * x into its tuple form,” but x already is in tuple form, and you need a way to tell Elixir to leave it alone. Example 13-3 uses the unquote/1 function do exactly that. (You may find this example in ch13/ex3-double.)

Example 13-3. Using quote to create a macro

defmodule Double do

defmacro double(x) do

quotedo

2 * unquote(x)

end

end

end

This says, “turn 2 * x into internal form, but don’t bother converting x; it doesn’t need quoting”:

iex(8)> c("double.ex")

double.ex:1: redefining module Double

[Double]

iex(9)> require Double

[]

iex(10)> Double.double(3 * 5)

30

To summarize: quote means “Turn everything in the do block into internal tuple format”; unquote means “Do not turn this into internal format.” (The terms quote and unquote come from the Lisp programming language.)

NOTE

If you quote an atom, number, list, string, or a tuple with two elements, you will get back the same item and not an internal format tuple.

WARNING

The most common mistake people make when writing macros is to forget to unquote arguments. Remember that all of a macro’s arguments are already in internal format.

Creating New Logic

Macros also let you add new commands to the language. For example, if Elixir didn’t already have an unless construct (which works as the opposite of if), you could add it to the language by writing the macro shown in Example 13-4, which is in ch13/ex4-unless.

Example 13-4. Creating a macro to implement the unless construct

defmodule Logic do

defmacrounless(condition, options) do

quotedo

if(!unquote(condition), unquote(options))

end

end

end

This macro takes a condition and options (in their internal form) and expands them to the internal code for an equivalent if statement with a reversed test for the condition. As in the previous example, the condition and options must remain in an unquote state, as they are already in internal form. You can test it in the shell:

iex(4)> c("logic.ex")

[Logic]

iex(6)> require(Logic)

nil

iex(7)> Logic.unless (4 == 5) do

...(7)> IO.puts("arithmetic still works")

...(7)> end

arithmetic still works

:ok

Creating Functions Programatically

Everything in Elixir has an internal representation, even functions. This means that a macro can take data as input and output a customized function as its result.

Example 13-5 is a simple macro create_multiplier that takes an atom and a multiplication factor as its input. It produces a function whose name is the atom you gave, and that function will multiply its input by the factor.

Example 13-5. Using a macro to programmatically create a function

defmodule FunctionMaker do

defmacro create_multiplier(function_name, factor) do

quotedo

defunquote(function_name)(value) do

unquote(factor) * value

end

end

end

end

You now need another module to invoke the macro.

defmodule Multiply do

require FunctionMaker

FunctionMaker.create_multiplier(:double, 2)

FunctionMaker.create_multiplier(:triple, 3)

def example do

x = triple(12)

IO.puts("Twelve times 3 is #{x}")

end

end

Once this is done, you can use the programmatically created functions:

iex(1)> c("function_maker.ex")

[FunctionMaker]

iex(2)> c("multiply.ex")

[Multiply]

iex(3)> Multiply.double(21)

42

iex(4)> Multiply.triple(54)

162

iex(5)> Multiply.example()

Twelve times 3 is 36

:ok

The entire example is in ch13/ex5-programmatic.

WARNING

You can’t define a function programmatically outside of a module or inside of a function.

You can even write a single macro that creates many different functions.If, for example, you wanted to have a separate drop/1 function for each planemo, you could have a macro that takes a list of planemos with their gravity constants and creates those functions. Example 13-6 will create functions mercury_drop/1, venus_drop/1, etc. from a keyword list. The entire example is in ch13/ex7-multidrop.

Example 13-6. Creating multiple functions with a macro

defmodule Functionmaker do

defmacro create_functions(planemo_list) do

Enum.map planemo_list, fn {name, gravity} ->

quotedo

defunquote(:"#{name}_drop")(distance) do

:math.sqrt(2 * unquote(gravity) * distance)

end

end

end

end

end

Again, you need another module to invoke the macro:

defmodule Drop do

require FunctionMaker

FunctionMaker.create_functions([{:mercury, 3.7}, {:venus, 8.9},

{:earth, 9.8}, {:moon, 1.6}, {:mars, 3.7},

{:jupiter, 23.1}, {:saturn, 9.0}, {:uranus, 8.7},

{:neptune, 11.0}, {:pluto, 0.6}])

end

Once compiled, the 10 new functions are available to you:

iex(1)> c("function_maker.ex")

[FunctionMaker]

iex(2)> c("drop.ex")

[Drop]

iex(3)> Drop.earth_drop(20)

19.79898987322333

iex(4)> Drop.moon_drop(20)

8.0

The entire example is in ch13/ex6-multidrop.

When (Not) to Use Macros

What you have seen so far are good examples of sample programs. Everything in this chapter could have been done more easily with simple Elixir functions. While you’re learning about Elixir, go wild and experiment with macros as much as you like. When you start writing programs for general use and are tempted to write a macro, first ask, “Could I do this with a function?” If the answer is “Yes” (and it will be, most of the time), then stick with functions. Use macros only when it will make the lives of people who use your code easier.

Why, then, has this chapter made such a big tzimmes about macros, if you aren’t encouraged to use them? First, Elixir itself uses macros extensively. For example, when you define a record, Elixir programatically generates the functions that let you access that record’s fields. Even def anddefmodule are macros!

More important, when you read other people’s code, you may find that they have used macros, and the information from this chapter will help you understand what they’ve written. (It’s sort of like learning a foreign language; there are phrases you may never have to say yourself, but you want to be able to understand them when someone says them to you.)

Sharing the Gospel of Elixir

While this concludes your introduction to Elixir, be aware that Elixir is a young language with a growing ecosystem, and there are many more features available for you to learn.

It may seem easy to argue for Elixir. The broad shift from single computers to networked and distributed systems of multiprocessor-based computing gives the Elixir/Erlang environment a tremendous advantage over practically every other environment out there. More and more of the computing world is starting to face exactly the challenges that Elixir and Erlang were built to address. Veterans of those challenges may find themselves breathing a sigh of relief because they can stop pondering toolsets that tried too hard to carry single-system approaches into a multisystem world.

At the same time, though, I’d encourage you to consider a bit of wisdom from Joe Armstrong: “New technologies have their best chance a) immediately after a disaster or b) at the start of a new project.”

While it is possible you’re reading this because a project you’re working on has had a disaster (or you suspect it will have one soon), it’s easiest to apply Elixir to new projects, preferably projects where the inevitable beginner’s mistakes won’t create new disasters.

Find projects that look like fun to you and that you can share within your organization or with the world. There’s no better way to show off the power of a programming language and environment than to build great things with it!