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.
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.
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))
Just use alists! They are well supported by your Scheme implementation. They show exactly who is paired with who.
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
quote
and second the empty list ()
, and it expands into
an alist with a single pair - first quote
and second ()
:scheme@(guile-user)> '((quote . ()))
$3 = ((quote))
Which is the same as the result of expanding (plist-as-alist/qq (quote ())
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 cons
es). 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.
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:
rest
of plist
.key
and val
, and the previous result.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 unquote
s 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