Hi,

Am 29.08.2009 um 21:48 schrieb ronen:

In a lot of cases its seems that macros are used even when a function
can do the same task,
Macros seems to be less readable than their functional counterparts &
more complex to write (to me at least).

Its clear that there are special cases in which macros are the perfect
solution (like partial evaluation of expressions, if-else constructs
etc..).

Id appreciate any thoughts on this topic.


Yes, there are situations where macros are required (or easing things tremendously). These are for example:
 * requiring control over evaluation (eg. and/or, if, cond, ...)
* requiring things concerning classes (eg. the class name or a method name, (proxy [~someinterface] [] ...)) * inlining code for performance reasons... (that's ugly, but most - if not all - steroid code is ugly..)

A somewhat grey area is eg. removing boilerplate. Most of the time the boilerplate code can be put simply in a function. A usual pattern which arises looks like that:

    (defn frobnicate
      [a-foo some-bar a-baz]
      ....)

    (defn with-some-foo*
      [foo-opts f & args]
      (let [xxx    (create-required-xxx foo-opts)
            a-foo  (setup-a-foo xxx foo-ops)]
        (try
          (apply f a-foo args)
          (finally
            (clean-up a-foo)))))

    (with-some-foo* some-opts frobnicate some-bar a-baz)

So it is perfectly possible to handle such a situation without macros. However you have always to define a function (if not already available...), which can also be a bit annoying. So we could write a macro instead.

    (defmacro with-some-foo
      [[local foo-opts] & body]
      `(let [opts#  ~foo-opts
             xxx#   (create-required-xxx opts#)
             ~local (setup-a-foo xxx# opts#)]
         (try
           ~...@body
           (finally
             (clean-up ~local)))))

    (with-some-foo [a-foo some-opts]
      (frobnicate a-foo some-bar a-baz))

(Please imagine a more elaborate body...)

However the macro code is now considerable more difficult. We have to care for multiple evaluation of `foo-opts` and gensyming `xxx`. Missing any of these can give raise to hard-to-track-down bugs. So we have to be very careful. Plus the following disadvantages:

* code duplication for each call, resulting in an increased executable size * recompilation of each call site required to realise changes (while in the function case, only the function has to be recompiled)

Having a macro expanding into the function call probably combines the best of two worlds.

    (defmacro with-some-foo
      [[local foo-opts] & body]
      `(with-some-foo* ~foo-opts (fn [~local] ~...@body)))

I would highly recommend this approach: do in the macro only what is *required* to be done there and put the rest into a function. This works for a surprisingly large number of macros. The rest can decided on a case-by-case basis, but in general: "If in doubt, leave it out". Prefer always a function, over a macro if possible.

Some things to consider when macros are necessary:

* Know the limits of macros! They only have compile time information. This can be quite tricky actually. Consider the following:

       (defmacro foo
         [ops]
         `(let [x# (~(ops :plus) 1 2)
                y# (~(ops :plus) 5 4)]
            [x# y#]))

       (foo {:plus #(+ %1 %2) :minus #(- %1 %2)})

Besides the point, that this is only an example nonsense macro: Where is the gotcha? *ticktackticktackbing* You suddenly have *two* anonymous `:plus` functions. The `ops` map does actually *not* contain anonymous functions, but the code necessary to define them! Since the evaluation in the macro happens differently than "the usual way" you can get caught be surprise.

* People - realising the previous point - most of the time come up with `eval` and hence just got deeper into trouble. You cannot eval locals and function arguments. This is a severe limitation. Also `eval` means bytecode generation, which might not be allowed by the host platform. The only real application of `eval` is the 'e' in 'repl'. Plus some rather special uses like genetic programming. But those really aren't the average representation of the "usual" program.

This is my turn on macros (mostly stolen from "scsh" ;P) and I have seldom problems with macros. I think the most important point is "Know the limits of macros!". You simply can't push them. But as always: YMMV.

Sincerely
Meikel
 

Attachment: smime.p7s
Description: S/MIME cryptographic signature

Reply via email to