Macros in Elixir and Clojure
To me Elixir and Clojure seem like sister programming languages. Both are modern, dynamic and functional languages that embrace concurrency and immutability. Both Elixir and Clojure provide rich metaprogramming capabilities through macros. To understand macros let us look at how typically code execution works in any language ---
Code → Lexical Analysis & Parsing → AST → Execution
When we write normal functions, we usually do not care about how they are evaluated / executed internally. However, with macros we can work with the AST (Abstract Syntax Tree) level directly and manipulate the code there which provides us with unprecedented flexibility to create new language constructs and DSLs.
To see the striking similarities between Clojure and Elixir macros we will take a simple example. We will write a macro in both languages that takes a piece of code and reports the time of execution around it.
To see measurable results we will take an algorithm that is time consuming, nothing better than calculating Collatz sequences. The problem statement -
The following iterative sequence is defined for the set of positive integers:
n → n/2 (n is even)
n → 3n + 1 (n is odd)
Using the rule above and starting with 13, we generate the following sequence: 13 → 40 → 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1
It can be seen that this sequence (starting at 13 and finishing at 1) contains 10 terms. Although it has not been proved yet (Collatz Problem), it is thought that all starting numbers finish at 1. Which starting number, under one million, produces the longest chain?
The code in both Elixir and Clojure is simple enough -
defmodule Euler_14 do
require Timer
defp calc(num, acc) do num = trunc(num) dec = num / 2 inc = (num * 3) + 1 r = rem(num, 2) case { num, r } do {1, _} -> length(acc) {_, 0} -> calc(dec, [dec | acc]) {_, 1} -> calc(inc, [inc | acc]) end end
defp pmap(collection) do collection |> Enum.map(&(Task.async(fn -> calc(&1, [&1]) end))) |> Enum.map(&Task.await/1) end
def collatz do 1..1_000_000 |> Enum.chunk_every(100) |> Enum.map(fn(arr) -> pmap(arr) end) |> List.flatten |> Enum.with_index(1) |> Enum.max_by(fn(t) -> elem(t, 0) end) |> IO.inspect end
end
And in Clojure -
(ns clj-fun.collatz)
(defn calc [num] (loop [n num acc [num]] (let [inc (+ 1 (* n 3)) dec (/ n 2)] (if (== n 1) {num (count acc)} (if (== 0 (rem n 2)) (recur dec (conj acc dec)) (recur inc (conj acc inc)))))))
(defn solve [] (apply max-key val (into {} (pmap calc (range 1 1000000)))))
Now lets build the timing macro in Elixir -
defmodule Timer do
defmacro time_it(name, do: block) do quote do start_time = Time.utc_now result = unquote(block) IO.puts "Elapsed time for #{unquote(name)}: #{Time.diff(Time.utc_now, start_time, :milliseconds)} milliseconds" result end end
end
As mentioned earlier, macros work at the AST level, so a macro gets the AST version of the code and then needs to return the manipulated AST version of the code. In Elixir we can produce the AST easily by quote, in example above we inject the code which we need to execute / time and merge it in our custom quoted AST using unquote. So to time our collatz function we can write it as (among other ways) -
def collatz do Timer.time_it "collatz" do 1..1_000_000 |> Enum.chunk_every(100) |> Enum.map(fn(arr) -> pmap(arr) end) |> List.flatten |> Enum.with_index(1) |> Enum.max_by(fn(t) -> elem(t, 0) end) |> IO.inspect endend
or from the REPL -
Timer.time_it "collatz", do: Euler_14.collatz
which gives us -
Elapsed time for collatz: 10184 milliseconds{525, 837799}
Clojure essentially uses the same concepts but with a slightly different syntax -
(defmacro time-it [name expr] `(let [start# (. System (nanoTime)) ret# ~expr] (prn (str "Elapsed time for " ~name ": " (int (/ (- (. System (nanoTime)) start#) 1000000)) " milliseconds")) ret#))
This is essentially the same idea as Elixir, ` is the same as quote and ~ is the same as unquote. Plus Clojure provides # to bind values safely within the macro.
Just for fun, the output of the Timer from Clojure is -
Elapsed time for collatz: 13102 milliseconds[837799 525]
That’s it. We looked at macros and built a simple one in Elixir and Clojure and studied their similarities. As usual, with great power comes great responsibility, macros are powerful but should only be used when a normal function cannot do the job.