Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure Macros: When can a function not duplicate a macro's behaviour?

Tags:

macros

clojure

I'm playing around with clojure macros and I'm finding that a lot of macro behavior I can just replicate with function composition.

A good example of this is the threading macro:

(defn add1 [n] (+ n 1))
(defn mult10 [n] (* n 10))

(defn threadline [arg]
  (-> arg
      add1
      mult10))

I can replicate this easily with a higher order function like pipe:

(defn pipe [& fns]
  (reduce (fn [f g] (fn [arg] (g(f arg)))) fns))

(def pipeline
  (pipe
   #(+ % 1)
   #(* % 10)))

There must be instances where a macro can not be replaced by a function. I was wondering if someone had some good examples of these sorts of situations, and the reoccurring themes involved.

like image 229
MFave Avatar asked Jun 10 '18 01:06

MFave


4 Answers

One important advantage of macros is their ability to transform code at compile-time without evaluating any of it. Macros receive code as data during compilation, but functions receive values at run-time. Macros allow you to extend the compiler in a sense.

For example, Clojure's and and or are implemented as recursive macros that expand into nested if forms. This allows lazy evaluation of the and/or's inner forms i.e. if the first or form is truthy, its value will be returned and none of the others will be evaluated. If you wrote and/or as a function, all its arguments would be evaluated before they could be examined.

Short-circuiting control flow isn't an issue in your pipe function example, but pipe adds considerable run-time complexity compared to -> which simply unrolls to nested forms. A more interesting macro to try to implement as a function might be some->.

I'm finding that a lot of macro behavior I can just replicate with function composition

If your functions are amenable to it, you can certainly replace a simple threading macro with function composition with comp, similar to "point free" style in other functional languages: #(-> % inc str) is functionally equivalent to (comp str inc) and #(str (inc %)).

It's generally advised to prefer functions when possible, and even when writing a macro you can usually farm out most of the "work" to function(s).

like image 87
Taylor Wood Avatar answered Nov 07 '22 15:11

Taylor Wood


The first macro I ever learned is a good example of a macro that can't be written as a plain function:

(defmacro infix [[arg1 f arg2]]
  (list f arg1 arg2))

(infix (1 + 2))
=> 3

Of course this exact macro would never be used in the wild, but it sets the stage for more useful macros that act as readability helpers. It should also be noted that while you can replicate a lot of basic macro's behavior with plain functions, should you? It would be hard to argue that your pipe example leads to easier to read/write code than, say, as->.

The "reoccurring themes" you're looking for are cases where you're manipulating data at compile-time ("data" being the code itself), not run-time. Any case that requires the function to take its argument unevaluated must be a macro. You can partially "cheat" in some cases and just wrap the code in a function to delay evaluation, but that doesn't work for all cases (like the infix example).

like image 28
Carcigenicate Avatar answered Nov 07 '22 16:11

Carcigenicate


Macros are not interchangeable with functions, but you examples are:

(macroexpand '#(+ % 1))
; ==> (fn* [p1__1#] (+ p1__1# 1))

The reason why it works is because the argument expect a function and you use a macro that becomes a function. However I know that cond is a macro. It cannot be replaced with a function implementation since the arguments of a function gets evaluated and the whole point of cond is to only evaluate some parts of the arguments in a specific order based on evaluation of their predicates. eg. making a recursive function with that would never terminate since the default case will also always be called before the body of the function cond is evaluated.

The whole point of macros is to expand the lamguage and since the evaluation is controlled by the result you can make all sorts of new features that would be impossible with function except if one passed all arguments as functions to delay evaluation.

like image 2
Sylwester Avatar answered Nov 07 '22 16:11

Sylwester


In any language, macros -- compile-time functions from code to code -- let you do three things:

  1. Define new binding forms (e.g. Clojure's destructuring let).
  2. Change the order of evaluation (e.g. or, and).
  3. Present a domain-specific language (e.g. Instaparse).

You can debate 3 -- whether implementing DSLs truly requires macros. Sure you can do code generators that are functions from text files to text files. Or, sure you can do Ruby style runtime DSLs. But if you want a DSL that's integrated into the compiler at compile-time, then macros are effectively your "compiler API".

Having said that, it makes sense to use macros only for these special purposes. Use functions and/or data-driven code as much as possible. Including to do work behind the "facade" provide by a macro.

like image 2
Greg Hendershott Avatar answered Nov 07 '22 16:11

Greg Hendershott