Clojure Macros: How Evil?

Dan asked me at the Chicago Clojure Meetup this week if Clojure macros tend to send developers into the death spiral of metaprogramming Ruby's various hooks did when we first discovered those.

tl;dr: no.

The canonical example of evil Ruby metaprogramming is everyone's favourite this-will-trim-8-lines-of-code hack: strings to method names. Have you ever written something like this?

x.send("#{a}_#{b}")

Sure you have. It's okay. We've all sinned. Are you prevented from doing this sort of thing in Clojure? Nope. No more than you are prevented from doing it in Java. But in Clojure and Java, the apparent innocence of Ruby's `send` method is revealed to be a sham: Reflection feels bad in these languages.

All of Ruby's other metaprogramming hooks have something in common: define_method, const_get, method_missing, every flavour of eval, monkey-patching... and all their friends... happen at runtime.



The behaviour of my first few Clojure macros confused me because I was accustomed to Ruby's runtime powers. Clojure's macros are expanded, go figure, at macro expansion time. As such, they have a few interesting and related properties:

  • macros feel like adding a feature to the language
  • ruby metaprogramming feels like mutating the language
  • macros cannot use runtime data to generate dynamic code
  • ruby metaprogramming requires runtime data to generate dynamic code
  • macros require a new way of thinking about code generation
  • ruby metaprogramming is just a higher-level imperative layer 
Macros also have a very clear usage pattern (see Christophe's wonderful (not= DSL macros) presentation from the first ClojureConj):

data formats > functions > macros

This is to say: Build your functions on top of your data. Build your macros on top of your functions. Macros should always be a convenience rather than a requirement. This one little rule is often enough to remind yourself that "write a macro!" usually isn't the solution you're looking for.

We're probably better off to avoid comparing Ruby's metaprogramming facilities with macros at all. It makes more sense to compare Ruby's hooks to Clojure's reflection and Ruby's eval to Clojure's eval -- neither of which I've seen used in a production Clojure application. Macros actually stand out on their own, since Ruby doesn't have an equivalent feature.

Can you still write a steaming pile of magic in Clojure? Of course. Once you suffocate your desire for elegance, you can reflect and eval and macro your way into a painful and confusing labyrinth of obfuscated code just as you can in any other modern language. But Clojure's libraries and language features usually display enough power on their own that you aren't tempted to shortcut your way into an impenetrable structural abstraction. At least, I haven't seen it yet.

2 comments:

Saager Mhatre said...

"Reflection feels bad in these languages."

Hehe! You just reminded me of that Java/Ruby flamewar on tw-swdev that I contributed to (in no small part) in my early days at TW.

"All of Ruby's other metaprogramming hooks have something in common..."

they have this in common with send right? Just making sure I read that right.

BTW, totally awesome pic! Those you're cats?

devin said...

Great post. I appreciated the way you explained this. Please write more on macros and metaprogramming. I think you've summed up so much of what I've been trying to convey in this short post.

Thank you.