A superflat vector drawing of a ceramic throne.
Kaka Farm!

The Kaka Farm Blog!

Syntax Rules Macros And Enabling Syntactic Sugar Addiction.

Date published:

Originally written as comments on https://codeberg.org/kakafarm/random-files-and-notes/src/branch/master/2025-04-04-plist-to-alist-conversion-with-macros/foo.scm on my random files and notes repository https://codeberg.org/kakafarm/random-files-and-notes/.

All code in this post is released under the Creative Commons's CC0 1.0 Universal License (https://creativecommons.org/publicdomain/zero/1.0/) with Zipheir's (AKA Wolfgang Corcoran-Mathe of https://sigwinch.xyz/) permission.

As always, if I wrote something especially stupid and/or wrong, and also if not, please send me public accusations, admonitions, exegesions, renunciations, and cute little chicken scribblings of cows and their tools as emails to yuval.langer@gmail.com and/or as Activitypub posts to @kakafarm@shitposter.world (aka https://shitposter.world/users/kakafarm/) on the Balkanised Activitypub Federation.

Why?

Someone on #systemcrafters-live said that Scheme's dotted alist syntax '((1 . 2) (3 . 4)) looks ugly in comparison to Clojure's {1 2 3 4}, so I had to write something to convert a plist to an alist. This is me enabling a syntactic sugar addict.

My attempt, plist-as-alist, was hokum.

It would construct the wanted result at runtime, not at macro expansion time:

scheme@(guile-user)> ,expand (plist-as-alist (a b c d))
$18 = (append
  (list (cons 'a 'b))
  (append (list (cons 'c 'd)) '()))

What I expected to see was:

scheme@(guile-user)> ,expand (plist-as-alist (a b c d))
$18 = '((a . b) (c . d))

It is also not a very interesting syntax - you cannot use quasiquotes in it, just convert a list of literal values to an alist at runtime.

(define-syntax plist-as-alist
  (syntax-rules ()
    [(_ ()) (quote ())]
    [(_ (first second rest ...))
     (append (list (cons (quote first) (quote second)))
             (plist-as-alist (rest ...)))]))

In that case, it is better to just define a procedure that would do the same, plus naturally support quasiquotes:

(define (plist->alist plist)
  (let loop ([plist plist]
             [accumulator '()])
    (cond
     [(null? plist)
      (reverse accumulator)]
     [else
      (let ([key (car plist)]
            [value (cadr plist)]
            [rest-of-key-values (cddr plist)])
        (loop rest-of-key-values
              (cons (cons key value) accumulator)))])))

So how would you convert forms like the following in a generic way?

`(a ,(+ 2 3) c d)
=>
`((a . ,(+ 2 3)) (c . d))

The real solution to this contrived problem.

Just use alists! They are well supported by your Scheme implementation. They show exactly who is paired with who.

The second best solution to this contrived problem.

Zipheir, AKA Wolfgang Corcoran-Mathe of https://sigwinch.xyz/, provides us with a solution as the plist-as-alist/qq macro (qq for quasiquote), and the auxiliary macros to which it calls.

Now when we feed the new macro with the same input, it expands to the literal value:

scheme@(guile-user)> ,expand (plist-as-alist/qq (a b c d))
$7 = '((a . b) (c . d))

We can also provide it with a list that contains quasiquotes:

scheme@(guile-user)> ,expand (plist-as-alist/qq (,(+ 2 3) b c d))
$6 = ((@@ (guile) cons)
 ((@@ (guile) cons) (+ 2 3) 'b)
 '((c . d)))

Notice that we do not quote nor quasiquote what we feed to the plist-as-alist/qq macro. If we do, we get weird things like:

scheme@(guile-user)> ,expand (plist-as-alist/qq '())
$1 = '((quote))

Which is the same as calling with:

scheme@(guile-user)> ,expand (plist-as-alist/qq (quote ()))
$2 = '((quote))

Why does that happen?

The syntax we passed to the macro was really a list of two elements

scheme@(guile-user)> '((quote . ()))
$3 = ((quote))

Which is the same as the result of expanding (plist-as-alist/qq (quote ())

An important note (read more about it on https://www.greghendershott.com/fear-of-macros/, and, of course, the RnRSes https://standards.scheme.org/):

In the second syntax-rules case, you can see what seems to be Scheme code, you see append, and list, and cons. This is misleading. There is no code being run when the syntax-rules macro is expanded. That (append ...) is returned as is and replaces the the plist-as-alist macro.

Here is another example, more dramatic:

scheme@(guile-user)> ,expand (plist-as-alist (a b c d e f g h))
$10 = (append
  (list (cons 'a 'b))
  (append
    (list (cons 'c 'd))
    (append
      (list (cons 'e 'f))
      (append (list (cons 'g 'h)) '()))))

When we use my macro, plist-as-alist, the returned expanded syntax is (append ...), as if you wrote the conversion from plist to alist meticulously by hand. The syntax-rules form operates on syntax objects, not Scheme list objects (those are made of nested conses). The first, second, and rest ... are patterns. append, list, quote, and cons here are not really procedure calls, but elements in the syntax template, a syntax template into which first, second, and rest ... are placed. The template does not run code, it is merely what the macro call be replaced with when the macro is expanded.

The implementation is as follows:

We first take the pattern plist and feed it to the auxiliary macro ptoa, together with an empty list. The empty list would be used within ptoa to construct the result.

(define-syntax plist-as-alist/qq
  (syntax-rules ()
    [(_ plist)
     (ptoa plist ())]))

The auxiliary ptoa takes two patterns, the plist and the current result.

It deconstruct the plist, two elements at a time as key and val.

It then calls recursively to itself with:

The base case is the empty plist, either when plist just happens to be an empty list, or after all its elements were transfered to the result pattern.

Notice that the result is constructed in reverse, which then needs to be reversed a second time if we want the alist to be in the order of the original plist, therefore we include a reverse step in reverse-ptoa.

(define-syntax ptoa
  (syntax-rules ()
    [(_ () result)
     (reverse-ptoa result ())]
    [(_ (key val . rest) result)
     (ptoa rest ((key . val) . result))]))

reverse-ptoa first reverses its input pattern in the same manner ptoa had reversed its input plist. When reverse-ptoa had constructed a result, it surrounds the result with quasiquote, allowing for any unquotes to be evaluated at runtime.

(define-syntax reverse-ptoa
  (syntax-rules ()
    [(_ () result)
     (quasiquote result)]
    [(_ (x . rest) result)
     (reverse-ptoa rest (x . result))]))
Tag feeds:

Kaka Farm by Yuval Langer is licensed under Attribution-ShareAlike 4.0 International