4
votes

I have read the "casting SPELs" tutorial, its russian version adapted for clojure at http://lisperati.planvita.com/... And up to this time I can't understand how the following macro works: (see http://lisperati.planvita.com/actions.html for russian version or http://lisperati.com/actions.html for original one for Lisp):

(defspel game-action [command subj obj place & args]
  `(defspel ~command [subject# object#]
     `(spel-print (cond (and (= location '~'~place)
                             (= '~subject# '~'~subj)
                             (= '~object# '~'~obj)
                             (have? '~'~subj))
                        ~@'~args
                        :else '(i cannot ~'~command like that -)))))

It is is used further like:

(game-action weld chain bucket attic
   (cond (and (have? 'bucket) (def chain-welded true))
              '(the chain is now securely welded to the bucket -)
         :else '(you do not have a bucket -)))

(game-action dunk bucket well garden
             (cond chain-welded 
                   (do (def bucket-filled true)
                       '(the bucket is now full of water))
                   :else '(the water level is too low to reach -)))

Here defspel - is just an alias for defmacro.

The reason to create the macro was to substitue the following functions:

(defn weld [subject object]
  (cond (and (= location 'attic)
             (= subject 'chain)
             (= object 'bucket)
             (have? 'chain)
             (have? 'bucket)
             (not chain-welded))
        (do (def chain-welded true)
            '(the chain is now securely welded to the bucket -))
        :else '(you cannot weld like that -)))
(defn dunk [subject object]
  (cond (and (= location 'garden)
             (= subject 'bucket)
             (= object 'well)
             (have? 'bucket)
             chain-welded)
        (do (def bucket-filled true)
            '(the bucket is now full of water))
        :else '(you cannot dunk like that -)))

I am completely confused with how this "game-action" macro works... Can anybody explain me all things (nested quotes) about it?

I have already read the following article - http://blog.8thlight.com/colin-jones/2012/05/22/quoting-without-confusion.html - it did not help...

macroexpand-1 scares me too...

This is its output for the weld game-action:

(clojure-magic-game.core/defspel weld [subject_1058_auto__ object_1059_auto_] (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure-magic-game.core/spel-print)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/cond)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/and)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/=)) (clojure.core/list (quote clojure-magic-game.core/location)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote attic)))))))) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/=)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list subject_1058_auto_)))) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote chain)))))))) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/=)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list object_1059_auto__)))) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote bucket)))))))) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure-magic-game.core/have?)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote chain))))))))))) (quote ((cond (and (have? (quote bucket)) (def chain-welded true)) (quote (the chain is now securely welded to the bucket -)) :else (quote (you do not have a bucket -))))) (clojure.core/list :else) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure-magic-game.core/i)) (clojure.core/list (quote clojure-magic-game.core/cannot)) (clojure.core/list (quote weld)) (clojure.core/list (quote clojure-magic-game.core/like)) (clojure.core/list (quote clojure-magic-game.core/that)) (clojure.core/list (quote clojure.core/-))))))))))))))

Even if to remove all namespaces and indent the output It will still look too complicated to understand for me:

(defspel weld [subject__1058__auto__ object__1059__auto__] 
  (seq (concat 
         (list (quote spel-print)) 
         (list (seq (concat 
                      (list (quote cond)) 
                      (list (seq (concat 
                                   (list (quote and)) 
                                   (list (seq (concat 
                                                (list (quote =)) 
                                                (list (quote location)) 
                                                (list (seq (concat 
                                                             (list (quote quote)) 
                                                             (list (quote attic)))))))) 
                                   (list (seq (concat (list (quote =)) 
                                                      (list (seq (concat 
                                                                   (list (quote quote)) 
                                                                   (list subject__1058__auto__)))) 
                                                      (list (seq (concat 
                                                                   (list (quote quote)) 
                                                                   (list (quote chain)))))))) 
                                   (list (seq (concat 
                                                (list (quote =)) 
                                                (list (seq (concat 
                                                             (list (quote quote)) 
                                                             (list object__1059__auto__)))) 
                                                (list (seq (concat 
                                                             (list (quote quote)) 
                                                             (list (quote bucket)))))))) 
                                   (list (seq (concat 
                                                (list (quote have?)) 
                                                (list (seq (concat 
                                                             (list (quote quote)) 
                                                             (list (quote chain))))))))))) 
                      (quote ((cond 
                                (and 
                                      (have? (quote bucket)) 
                                      (def chain-welded true)) 
                                (quote (the chain is now securely welded to the bucket -)) 
                                :else (quote (you do not have a bucket -))))) 
                      (list :else) 
                      (list (seq (concat 
                                   (list (quote quote)) 
                                   (list (seq (concat 
                                                (list (quote i)) 
                                                (list (quote cannot)) 
                                                (list (quote weld)) 
                                                (list (quote like)) 
                                                (list (quote that)) 
                                                (list (quote -))))))))))))))
1
English Clojure version starts at lisperati.com/clojure-spels/casting.html.A. Webb
Found a write up by @amalloy at Clojure: macro-writing macros on nested macros -- how to avoid them and how to understand them. Recommended reading. Includes the memorable, "Nested macros are like amputations: they're very painful, and there's usually a better solution, but on rare occasions you have no choice."A. Webb
Thanks, friend! Will check!yashaka

1 Answers

4
votes

Don't try to learn about macros or Clojure from this.

  1. Quoting the English Lisp version

    Notice how ridiculously complex this SPEL is- It has more weird quotes, backquotes, commas and other weird symbols than you can shake a list at. More than that it is a SPEL that actually cast ANOTHER SPEL! Even experienced Lisp programmers would have to put some thought into create a monstrosity like this (and in fact they would consider this SPEL to be inelegant and would go through some extra esoteric steps to make it better-behaved that we won't worry about here...)

    The point of this SPEL is to show you just how sophisticated and clever you can get with these SPELs. Also, the ugliness doesn't really matter much if we only have to write it once and then can use it to make hundreds of commands for a bigger adventure game.

    Translation: This is write-only garbage. In what is meant to be a tutorial introduction to the language, the author has chosen poor design in order to showcase an unnecessarily complex macro and wasted an opportunity to discuss the judicious use of macros and the power of first class and higher order functions.

  2. The Clojure version is a transliteration rather than a proper translation. One example: setf inside a function was transliterated into a def inside a function. This is bad Clojure.

Off my soapbox now

The Quoting Without Confusion article you linked to is worth another read or two.

Rather than use strings, "bucket", or keywords, :bucket, which would be more appropriate for his purpose, the "Casting SPELs" author is using quoted symbols -- 'bucket also known as (quote bucket).

So the author needs a macro expansion like

(... (quote bucket) ...)

But

user=> `(... 'bucket ...)

does name-space qualification:

(... (quote user/bucket) ...)

So, you wind up having to

user=> `(... '~'bucket ...)

to get

(... (quote bucket) ...)

the trick being the extra ~' as explained in the Quoting Without Confusion article.

But as the game-action macro author has nested two-deep in syntax quotes, you have another layer to unwind, which means another set of ~'

user=> ``(... '~'~'bucket ...)

The first ' is the quote you want, the two sets of ~' are to escape name-space qualification twice, once for each level of syntax quote ``.

This yields

(clojure.core/seq 
  (clojure.core/concat 
    (clojure.core/list (quote ...)) 
    etc...

Yikes! But taking the advice of Quoting Without Confusion when dealing with nested syntax quotes:

user=> (eval *1)

(... (quote bucket) ...)

and we see all this construct is just to give us what we wanted.