[racket] Onlisp's condlet macro in Racket

From: Vincent St-Amour (stamourv at ccs.neu.edu)
Date: Wed Jan 2 11:34:56 EST 2013

At Tue, 1 Jan 2013 17:59:21 -0500,
Sean Kanaley wrote:
> While I've ultimately succeeded in having it return the correct output for
> a sample input, I'm not positive it's correct in general and I *am *positive
> it's written poorly, as I don't fully understand both syntax-case and
> syntax objects vs. datums.  If someone could look it over and provide a
> more canonical version, I would be grateful.
> 
> Here's how the macro works:
> 
> > (condlet (((= 1 2) (x (princ ’a)) (y (princ ’b)))
>             ((= 1 1) (y (princ ’c)) (x (princ ’d)))
>             (t (x (princ ’e)) (z (princ ’f))))
>     (list x y z))
> *CD
> (D C NIL)*
> 
> Before I post the horrible racket code, I will explain the problems I'm
> having with macros in general:
> 
> Problem 1, separate phases:  I have a remove-duplicates-by function that
> would be great to have globally, but it seemingly must be written locally.

You can use `define-for-syntax' to define functions to be used at
expansion time. More generally, `begin-for-syntax' runs code at
expansion time. You can also `require' libraries for syntax time by
using the `for-syntax' `require' sub-form.

In your example, you could define `remove-duplicates-by' like this:

    (define-for-syntax (remove-duplicates-by f l)
      (let R ((l l))
        (if (null? l)
            null
            (cons (car l) (R (remove* (list (car l))
                                      (cdr l)
                                      (λ (a b)
                                        (eq? (f a) (f b)))))))))

and you would be able to use it in your macro.

> Problem 2: You can't use a pattern variable outside of a pattern, so you
> have to syntax-ify it with #', but then you can't access the associated
> s-exp without removing the syntax.

This blog post explains the relationship between syntax objects and
s-expressions:
    http://blog.racket-lang.org/2011/04/writing-syntax-case-macros.html

> The way to bind things to null by
> default is to get every id and output the obvious let statement, except ids
> might be repeated so you have to remove duplicates (enter problem 1).  It's
> remove-duplicates-BY because the removal happens by syntax->datum'ing each
> identifier-syntax-thing since it can't appear outside of a pattern.

Comparing identifiers using `syntax->datum' can equate identifiers that
are different, but happen to have the same name. Identifiers have their
own comparison functions that do the right thing. `free-identifier=?' is
what you want here.

Also, `racket/list''s `remove-duplicates' takes an optional comparison
argument, so you can pass it `free-identifier=?'. It also takes a
`#:key' argument, which makes it act like your `remove-duplicates-by'.
To do this, you would need to `(require (for-syntax racket/list))'.

> Problem 2: How to remove a portion of the macro code into a separate
> transformer function?  It's kind of annoying having a whole block of code
> relegated to cleaning up the duplicate ids inside of the let it expands
> into.  That would ideally be written "let #,(remove-dups #'(c cs...))" or
> similar...some kind of sub-macro to handle just getting ids.  I thought
> that's what let-syntax or nested define syntaxes were for but I get phase
> errors or preposterous, very dark errors like "lambda not bound".

The blog post I linked above has an explanation of phase distinctions.

In general, if you want to use a function (or a macro) in a macro (as
opposed to expanding into it), that function (or macro) needs to be
imported for syntax. For convenience, `#lang racket' provides all of
`racket/base' for syntax.

Vincent


Posted on the users mailing list.