Arc Forumnew | comments | leaders | submitlogin
Clojure Anaphoric Macros (github.com)
4 points by i4cu 2171 days ago | 10 comments


5 points by akkartik 2171 days ago | link

"In `acond`, `aif` and `awhen`, `%test` or `%t` gets replaced with the 'test' form. `%then` gets replaced by the 'then' form, and `%else` by the 'else' form."

    (aif 9 (+ 9 %else) (+ 10 %test))
    ; => (if 9 (+ 9 (+ 10 9)) (+ 10 9))
"If nested you can access the previous level by doubling the first letter of the symbol. For example, '%ttest' would get you the previous [containing?] 'test' form, while '%eeelse' would get you the 'else' form 2 levels up. In the `aand` and `aor` macros you can reference arguments by using a symbol of form `<star><num>` where 'num' is the 1-index of the argument. Previous levels are accessed by doubling the `<star>` character. So the second 'test' form of an `aand` can by accessed with `<star>2` and the third argument of the previous [containing?] `aand` would be `<star><star>3`." [Working around the crappy pseudomarkdown here.]

    (aand (+ 30 20) *1)
    ; => (and (+ 30 20) (+ 30 20))

    (aand 1 2 "third" (aand 33 **3))
    ; => (and 1 2 "third" (and 33 "third"))
Utterly bonkers, and I love the hack :)

-----

3 points by akkartik 2169 days ago | link

Staring at these examples again, another thought occurs to me: do these macros cause multiple evaluation, or are the equivalences above just loose? Can anybody tell? I can't tell just from skimming the implementation.

If they're doing multiple evaluations they're a lot less useful than Paul Graham's original anaphoric macros even if they seem superficially more powerful/expressive.

-----

3 points by i4cu 2169 days ago | link

> do these macros cause multiple evaluation

from the read-me:

"Note that as of right now the symbols are replaced by copying in the expression it references, not by binding to a common variable. Hence not suitable for using with expressions that cause side-effects or involve a lot of computation. That will be changed soon."

I presume, by reading this, these expressions will be evaluated each time they are triggered within the operation, so the answer is - yes they will. But, as the read-me also suggests, this is a "WORK IN PROGRESS" and the author has stated intentions to assign variables, which would solve the issue.

-----

2 points by hjek 2170 days ago | link

If you include * whitespace * between words and stars, they are not parsed as an emphasized section.

-----

3 points by i4cu 2171 days ago | link

Saw this on reddit and given that it's derived from Arc I thought I'd post it over here.

Certainly these are much more involved than my functions. My aif for example is:

  (defmacro aif [expr & body]
    `(let [~'it ~expr]
       (if ~'it
         (do ~(first body))
         (do ~@(next body)))))
Actually, when I started in Clojure I was using these anaphoric operations a lot, but most of my code has moved away from them (for no particular reason, I just haven't needed them much I guess).

-----

2 points by hjek 2171 days ago | link

Racket doesn't have anaphoric macros as part of the core language (although there is a module for that), so that's a bit difficult getting used to when coming from Arc, but I find that pattern matching[0] can be used to the same effect:

    (match 123
       ((and (? even?) it) (~a it " is even"))
       (it (~a it " is odd")))
[0]: https://docs.racket-lang.org/reference/match.html

-----

3 points by adas 2163 days ago | link

Author here. Glad to see some interest, sometimes I lurk here and now I feel bad not having submitted here myself.

Interestingly the "%else" would actually be cataphoric, as you refer to what comes next rather than before. So "co-referential macros" would be more fitting if you want to stick with the linguistics analogy. But that'd be too exotic of a term.

And yes as akkartik notes, it causes multiple evaluations right now, mostly just laziness and indecision on my part. I'll probably be giving control over this. Here's a real example of code where you actually want current behavior:

  (aif (ns-resolve *ns* 'specs)
    (let [c (compile spec :name name)]
      (swap! (var-get %test) update (:property c) set/union #{c}))
    (do (intern *ns* 'specs (atom {}))
         %then))
Would love to hear if anyone has some crazy ideas. Beyond just conditionals too.

-----

2 points by akkartik 2162 days ago | link

Thanks for that example. I see now that you mostly only need to worry about multiple evaluation for the `%test` branch; `%then` and `%else` should be exclusive anyway, and I'm not concerned about the growth in macroexpanded code when duplicating a few s-expressions.

You could still have repeated use within a '%then' or '%else' block:

    (aif (test)
      (something)
      (do %then %then))
But it should suffice to perform one evaluation in each branch. Cool! That seems simpler than some of the alternatives I'd been thinking about. I'd try to evaluate everything ahead of time and then realize that I shouldn't run `%else` if the `%test` returns a truthy value.

-----

2 points by adas 2162 days ago | link

Didn't consider a double `%then` or `%else`, thanks. Raises some interesting problems.

Generally the plan is to have a special character control whether it gets bound or inlined, probably `!`. So say `%test!` would get inlined like right now, while using `%test` would bind (and doing both would also be possible). But for a `%then` you'd generally only want to bind if there's 2+ so I could count usages instead.

-----

3 points by adas 2148 days ago | link

Mostly side-effects free now, controllable via `!` as noted below (in most cases). Also added tests that compare macroexpand results where feasible. Big cleanup next.

-----