Rick77
Rick77

Reputation: 3211

Macro composition

I'm playing with Clojure macros a bit, specifically trying a macro version of map, which I imaginatively named "mmap":

(defmacro mmap [f coll]
    `(vector ~@(map #(list f %) coll)))

Please don't delve upon the reasons why I may be doing this. It's just a minimal reproducible example.

It works fine as long as a collection is passed to it, but when I try to compose it with another macro, mmap slices and dices the sexp with the macro itself, not its expansion.

I would like "mmap" to work like this:

(mmap inc [1 2 3 4])            => [2 3 4 5]
(mmap dec (mmap inc [1 2 3 4])) => [1 2 3 4] NOPE. See the edit.

So the question is: is there a way to force the expansion of a macro passed as an argument to another macro? In which case, how?

Thank you for any insight!

EDIT I just noticed that my example doesn't make sense, passing the result of the mmap macro would be like calling:

(mmap inc (vector 1 2 3 4))

which makes no sense in this specific case, but I still want to know if it can be done somehow

EDIT2 the use of a [1 2 3 4] array instead of a symbol, like "arg" was meant to convey that the whole thing is done at compile time, not runtime.

Upvotes: 2

Views: 207

Answers (2)

Alan Thompson
Alan Thompson

Reputation: 29958

Functions always evaluate their arguments before invoking the function, leading expressions to be evaluated in the familiar "inside out" pattern:

(/ (inc 3) (dec 3))  ; orig
(/ 4 2)              ; args sent to `/` function
2                    ; final result

The purpose of a macro is to avoid evaluating the arguments before calling the function, leading to expressions being evaluated "outside in". This allows one to compose macros as if each were a new language feature:

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test))

(defmacro infix
  [[l op r]]
  `(~op ~l ~r))

(defmacro snoop
  [& forms]
  `(let [result# ~@forms]
     (do
       (println "snoop => " result#)
       result#)))

(dotest
  (spyx (infix (2 + 3)))
  (spyx (infix (5 - 3)))
  (newline)

  (snoop (+ 1 2))
  (snoop (infix (4 + 5)))
  (newline)

  (println :last
    (infix ((snoop 4) + (snoop 5)))))

with result

(infix (2 + 3)) => 5
(infix (5 - 3)) => 2

snoop =>  3
snoop =>  9

snoop =>  4
snoop =>  5
:last 9

So that final example has an intermediate stage that looks like:

(println :last
  (+ (snoop 4) (snoop 5)))

where can see that infix has been evaluated before the 2 snoop calls. This is exactly the strength of macros, and they would be useless without it.

You could maybe create a new macro that worked like:

(defmacro mmap
  [fns coll]
  `(let [comp-fn# (comp ~@fns)]
     (mapv comp-fn# ~coll)))

(mmap [inc inc inc] [1 2 3]) => [4 5 6]

While the above mmap works, it is really an abuse of the macro system, and might fail if you give it something other than compile-time constants.

A better alternative for basic functions is simple memoization of a normal function:

(def do-stuff
  (memoize ; will only execute on the first usage
    (fn
      [coll]
      (mapv #(* 2 %) (mapv inc coll)))))

(do-stuff [1 2 3 4]) => [4 6 8 10]

If you think even a single call at runtime is too much, I would suggest manually running the "macro-like" code in a pre-compilation step and saving the result in new "source file" that could then be compiled as usual (perhaps you are pre-computing a bunch of prime numbers, for example).

The upshot is that the macro system is intended only for manipulating code in order to add new language features. Any other goal is better served using a different technique.


Regarding mapv

I also discovered mapv by accident and only after many months.

Be sure to bookmark the Clojure CheatSheet and always keep a browser tab open to it. Study it regularly until you can remember each of the functions. :)

Also be aware that some more obscure items are not listed there!


Update:

The "killer feature" of macros is to add new language features. In Java, et al, one can only add new libraries. As an example, spyx above is a macro very similar to the snoop example. For other examples see with-exception-default, vals->map, and it-> from the Tupelo library. In fact, many "core" Clojure features like the for expression or the and & or logical operators are actually macros that any application program could write (or modify!).

Upvotes: 4

coredump
coredump

Reputation: 38789

Macroexpansion works from the outermost form and is called repeatedly until a fixpoint is reached. That means that from your macro, you are given unexpanded code. This might be counter-untuitive if you expect it to work like functions, where arguments are evaluated before the function is called. With macro, nothing is expanded and nothing is evaluated, making it possible for your to interpret the underlying code as you wish.

But you are also free to call macroexpand yourself from your macro, which will ensure that nested forms are expanded first. In your case, that means that with literal values, you will get data back, and you could process the macroexpanded value as-if it was given directly to your macro.

But note that this work only if the innermost call to your macro is given a literal value and produce a literal value back. In any case, a macro cannot know which value will be available at runtime. Usually people then try to call eval directly on forms, but this is a bad idea, because code depends on the runtime context, it makes no sense in general to evaluate code during macroexpansion/compilation that is supposed to be evaluated at runtime.

Upvotes: 1

Related Questions