mattyw

talkative, friendly, programmer

Simple Macros in Clojure and Elixir

| Comments

Don’t write macros is a pretty good rule, but when you’re learning a language it’s quite a fun little exercise. One such exercise I like to try is this:

Write a function so that I can call system commands without having to wrap them in quotes.

So rather than doing this:

cmd([“grep”, “-rin”, “foo”, “./”])

I can do this:

cmd([grep, -rin, foo, ./])

Clojure

In lisps like clojure, the solution is very elegant, thanks to the minimal syntax, you end up with something like:

1
(call grep -rin foo "./")

And the macro to do it is also pretty elegant:

1
2
3
(defmacro mcall [& args]
  (let [sym (gensym)]
  `(apply sh (map str '~args))))

Not too much magic (as far as macros are concerned) here. Make a unique symbol with (gensysm) to keep our macro hygienic. We syntax quote our whole list using ` to not evaluate anything unless we say so. That means we have to do some magic with our args parameter: ‘~args. ~ here means evaluate args (so we get the symbols we passed in) but then we quote with ’ so that we don’t evaluate these symbols. That’s the magic that lets us not need quotes around our arguments. The rest of the code just puts them in the right place by mapping our args list against the str function and applying it to sh. This is how you use it:

1
2
(prn (mcall ls -l))
(prn (mcall grep -rin not "./")) ; We still need quotes around ./ because it's not valid syntax

By coincidence I blogged about clojure macros almost exactly one year ago: http://mattyjwilliams.blogspot.co.uk/2012/08/another-clojure-macro-tutorial-that-no.html

Elixir

Elixir is a lovely little language I’ve been playing with recently, so naturally I wanted to try out the macros. I needed to ask a few questions on the irc channel (thanks ericmj and true_droid!), but I got it cracked:

1
2
3
4
5
6
defmacro mcall(args) do
    call = Enum.map_join(args, " ", Macro.to_string(&1))
    quote do
      System.cmd(unquote(call))
    end
end

Again, not much magic, map_join maps our list using the Macro.to_string function then joins at the end, this is done outside of the quote since we will work it out at compile time. We then pass this to System.cmd in a quote block – which means don’t evaluate this yet, that will happen when we call mcall at runtime.

1
2
IO.inspect MacroTest.mcall([ls, -l])
IO.inspect MacroTest.mcall([grep, -rin, def, "./"]) # We still need quotes around ./ because it's not valid syntax

Comments