Is there any equivalent way to emulate symbol-macrolet of ANSI Common Lisp with the Racket's Syntax?

Hello.

As the title says, is there any equivalent way to emulate symbol-macrolet of ANSI Common Lisp with the Racket's Syntax?

Thanks.

I have never really used Common Lisp, but, from a quick reading of the HyperSpec, here is an implementation:

#lang racket
;; SPDX-License-Identifier: CC0-1.0

(require (for-syntax syntax/parse
                     syntax/transformer)
         rackunit
         syntax/macro-testing)

(define-for-syntax (mutable-variable-transformer stx)
  (make-variable-like-transformer
   stx
   (syntax-parser
     #:track-literals
     #:literals [set!]
     [(set! _ rhs)
      #`(set! #,stx rhs)])))

(define-syntax (symbol-macrolet stx)
  (define-syntax-class bindings
    #:description "sequence of distinct binding pairs"
    (pattern ((~describe "binding pair"
                         [symbol:id expansion])
              ...)
      #:fail-when (check-duplicate-identifier
                   (syntax->list #'(symbol ...)))
                  "duplicate symbol macro name"))
  (syntax-parse stx
    [(_ :bindings body ...+)
     #'(let-syntax ([symbol (mutable-variable-transformer
                             #'expansion)]
                    ...)
         body ...)]))

(check-equal?
 (symbol-macrolet ([x 'foo])
   (list x (let ([x 'bar]) x)))
 '(foo bar))

(check-equal?
 (symbol-macrolet ([x '(foo x)])
   (list x))
 '((foo x))) ; hyperspec says ((FOO x))

(check-equal?
 (let ([bx (box 1)])
   (define-syntax implicit-bx
     (make-variable-like-transformer
      #'(unbox bx)
      #'(λ (v) (set-box! bx v))))
   (symbol-macrolet ([x implicit-bx])
     (list x (begin (set! x 2) x))))
 '(1 2))

(check-exn
 (match-lambda
   [(exn:fail:syntax "set!: not an identifier"
                     _
                     (list (app syntax->datum '(quote foo))))
    #t]
   [_
    #f])
 (λ ()
   (convert-syntax-error
    (symbol-macrolet ([x 'foo])
      (set! x 2)))))

The last two examples show my attempt to translate the specification that, "when the forms of the symbol-macrolet form are expanded, any use of setq to set the value of one of the specified variables is treated as if it were a setf." However, as the final example illustrates, since Racket doesn't really have Common Lisp's notion of a place, for an expansion that isn't just an identifier, this will fail with a syntax error, and the message will include the expansion, not the source form. A less pedantic but more useful implementation might replace mutable-variable-transformer with just make-variable-like-transformer (i.e. without the optional second argument), which would raise a better syntax error, e.g. (for the last example) set!: cannot mutate identifier at: x in: (set! x 2). Of course, you could also implement a variant of set! that supports something like Common Lisp's places or use an existing one, like srfi/17.

You might also be interested in assignment transformers created by make-set!-transformer or prop:set!-transformer, the lower-level mechanism used to implement make-variable-like-transformer.

Hello.

Phew! It works! Great! Black Magic!
May God bless you.

At first, I assumed that somethings listed, though I am not familiar with, under syntax-case in the Scheme Programming Language 4th Edition might work, but I could not find the way to bind a symbol itself with a certain syntax expansion.
I would read and study make-variable-like-transformer, syntax-parser, and syntax-parse, which are not included in the book, in Racket's document.

Anyway, thank you very much. You shined a light over my road ahead.

Slight nit: you might want to use

(mutable-variable-transformer (quote-syntax expansion))

rather than

(mutable-variable-transformer #'expansion)

Since the former will support

(symbol-macrolet ((x ...))
   (let ([x 1]) x))

while the latter fails.

But yeah, I don’t know what @cametan is actually looking for. Is it let-syntax? Or is it make-variable-like-transformer? One can be used without the other.

For R6RS, these are called identifier macros, and support for set! uses identifier-syntax or make-variable-transformer, which is almost identical to Racket's make-set!-transformer. These are discussed in chapter 8 of TSPL4.

Here is an example:

#!/usr/bin/env scheme-script
;; appease Guile: !#
#!r6rs
;; SPDX-License-Identifier: CC0-1.0
(import (rnrs))

(define-syntax symbol-macrolet
  (syntax-rules ()
    [(_ ([symbol expansion] ...) body0 body ...)
     (let-syntax
         ([symbol (let ([exp #'((... ...) expansion)])
                    (make-variable-transformer
                     (lambda (stx)
                       (syntax-case stx (set!)
                         [(set! who rhs)
                          (if (identifier? exp)
                              #`(set! #,exp rhs)
                              (syntax-violation (syntax->datum #'who)
                                                "cannot mutate identifier"
                                                stx
                                                #'who))]
                         [(_ arg (... ...))
                          #`(#,exp arg (... ...))]
                         [id
                          (identifier? #'id)
                          exp]))))]
          ...)
       body0 body ...)]))

(define-syntax check-equal?
  (syntax-rules ()
    [(_ actual-expr expected-expr)
     (let ([src 'actual-expr]
           [actual actual-expr]
           [expected expected-expr])
       (unless (equal? actual expected)
         (display   "!! Error: " (current-error-port)) (write src (current-error-port))
         (display "\nExpected: " (current-error-port)) (write expected (current-error-port))
         (display "\nActual:   " (current-error-port)) (write actual (current-error-port))
         (newline)))]))

;; Syntax violation:
#;(symbol-macrolet ([x 'foo])
    (set! x 2))

(check-equal?
 (symbol-macrolet ([x 'foo])
   (list x (let ([x 'bar]) x)))
 '(foo bar))

(check-equal?
 (symbol-macrolet ([x '(foo x)])
   (list x))
 '((foo x))) ; hyperspec says ((FOO x))

(check-equal?
 (let ([bx (vector 1)])
   (define-syntax implicit-bx
     (make-variable-transformer
      (lambda (stx)
        (syntax-case stx (set!)
          [(set! _ rhs)
           #'(vector-set! bx 0 rhs)]
          [(_ arg ...)
           #'((vector-ref bx 0) arg ...)]
          [_
           #'(vector-ref bx 0)]))))
   (symbol-macrolet ([x implicit-bx])
     (list x (begin (set! x 2) x))))
 '(1 2))

(check-equal?
 (symbol-macrolet ((x ...))
   (let ([x 1]) x))
 1)

;; Written for https://racket.discourse.group/t/2966

Good catch, and the example helped! (You'll see that I worked around this specific issue in the R6RS version through the (... ...) escape, but quote-syntax is The Right Thing in Racket.)

I had in fact thought about this and even looked up the old mailing list post by @ryanc that got me to really understand expanding to quote and quote-syntax and stop getting ?: literal data is not allowed; no #%datum syntax transformer is bound errors. What threw me off was then over-thinking the fact that defining syntax-rules in terms of syntax-case uses syntax, not quote-syntax. For syntax-rules, though, what the user writes on the right-hand side really is (in @ryanc's terms) a Syntax-Template, and template features like ... and pattern variables are supposed to have their Syntax-Template meaning, even though the Syntax-Template context comes from being a subform of syntax-rules, rather than of syntax, quasisyntax, syntax/loc, etc. In contrast, a right-hand side of symbol-macrolet (at least AIUC) is a literal fragments of program, and thus needs to expand to, in @ryanc's words, "an Expression that produces the same syntax object at run time", where "run time" in this case is the generated macro's compile time.

Hello.

Here is the situation and this is a sort of experiment.
I started to re-read On Lisp, which a Lisp hacker, Paul Graham wrote.
He there showed a macro named on-cdrs, which abstracts a cdr-recursive function.

(defun lrec (rec & optional base)
  (labels ((self (lst)
             (if (null lst)
                 (if (functionp base)
                     (funcall base)
                     base)
                 (funcall rec (car lst)
                          #'(lambda ()
                              (self (cdr lst)))))))
    #'self))

(defmacro alrec (rec &optional base)
  (let ((gfn (gensym)))
    `(lrec #'(lambda (it ,gfn)
               (symbol-macrolet ((rec (funcall ,gfn)))
                 ,rec))
           ,base)))

(defmacro on-cdrs (rec base &rest lsts)
  `(funcall (alrec ,rec #'(lambda () ,base)) ,@lsts))

However, technically speaking, you would notice the macro, on-cdrs, is basically foldr wrapped with the syntax-case of Racket:

(define-syntax (on-cdrs x)
  (syntax-case x ()
    ((on-cdrs expr base lst ...)
     (with-syntax ((it (datum->syntax #'on-cdrs 'it))
                   (rec (datum->syntax #'on-cdrs 'rec)))
       #'(foldr (lambda (it rec)
                  expr) base lst ...)))))

Otherwise, you can say this macro is foldr using anaphora(it and rec), instead of using a lambda expression.
In most cases, this macro is O.K.; however Paul Graham gave this example.

;; ANSI Common Lisp Version
(defun maxmin (args)
  (when args
    (on-cdrs (multiple-value-bind (mx mn) rec
               (values (max mx it) (min mn it)))
             (values (car args) (car args))
             (cdr args))))

;; Racket Version
(define (maxmin args)
  (unless (null? args)
    (on-cdrs (let-values (((mx mn) rec))
               (values (max mx it) (min mn it)))
             (values (car args) (car args))
             (cdr args))))

This does not properly work, because foldr is not planned to receive values.
Of course, practically speaking, I believe that nobody likes to use values as above, because what a function returns is important, but internally, passing values does not seem a good idea; thus everyone may like to write the code like this:

(define (maxmin args)
  (unless (null? args)
    (apply values
           (on-cdrs (match-let ((`(,mx ,mn) rec))
                      `(,(max mx it) ,(min mn it)))
                    `(,(car args) ,(car args))
                    (cdr args)))))

I think this should be the right code...but, isn't it frustrating? Is Racket lost? Do we have to re-implement foldr?
Racket has delay and force, thus I believe we can have such a framework...:

(define-syntax (on-cdrs x)
  (syntax-case x ()
    ((on-cdrs expr base lst ...)
     (with-syntax ((it (datum->syntax #'on-cdrs 'it))
                   (rec (datum->syntax #'on-cdrs 'rec)))
       #'(force
          (foldr (lambda (it gfn)
                  #| here, need something syntactically binding rec to (force gfn) |#
                  (delay expr)) (delay base) lst ...))))))

And here, I was stuck.
As you implied, at first I thought using let-syntax is O.K., but embarrassing to admit, I misunderstood that let-syntax is something like symbol-macrolet of ANSI Common Lisp:

 (define-syntax (on-cdrs x)
  (syntax-case x ()
    ((on-cdrs expr base lst ...)
     (with-syntax ((it (datum->syntax #'on-cdrs 'it))
                   (rec (datum->syntax #'on-cdrs 'rec)))
       #'(force
          (foldr (lambda (it gfn)
                  (let-syntax ((rec (syntax-rules ()
                                      ((_) (force gfn)))))
                    (delay expr))) (delay base) lst ...))))))

Yes, it works, but every time we use this version, the anaphora "rec" has to be sandwiched between a parenthesis and a sisehtnerap.

(define (maxmin args)
  (unless (null? args)
    (on-cdrs (let-values (((mx mn) (rec))) ; rec always has to have a ( and a )
               (values (max mx it) (min mn it)))
             (values (car args) (car args))
             (cdr args))))

That is why I was looking for a technique to bind a symbol to something, directly expanding into an expression in the context, or a substitution for ANSI Common Lisp's symbol-macrolet in Racket.

Thanks.

O.K. I will check that out.
Thank you.

My go at Racket implementations of these alrec and on-cdrs macros, using syntax parameters for the it and rec forms (Which is generally a better approach than injecting those symbols into the macro). These versions work with expressions that return multiple values as well as single-valued ones without needing the extra parens around rec.

#lang racket/base

(require racket/contract racket/stxparam syntax/parse/define
         (for-syntax racket/base))

(define (lrec rec [base '()])
  (define (self lst)
    (if (null? lst)
        (if (procedure? base)
            (base)
            base)
        (rec (car lst) (lambda () (self (cdr lst))))))
  self)

(define-syntax-parameter it
  (lambda (stx) (raise-syntax-error #f "use of it outside anaphoric macro")))
(define-syntax-parameter rec
  (lambda (stx) (raise-syntax-error #f "use of rec outside alrec")))

(define-syntax-parse-rule (alrec e:expr (~optional base:expr))
  (lrec (lambda (x gfn)
          (syntax-parameterize ([it (make-rename-transformer #'x)]
                                [rec (lambda (stx) #'(gfn))])
            e))
        (~? base '())))

(define-syntax-parse-rule (on-cdrs e:expr base:expr lsts:expr ...)
     ((alrec e (lambda () base)) lsts ...))


;;; Example usages
(define (our-length lst)
  (on-cdrs (add1 rec) 0 lst))

(define/contract (maxmin args)
  (-> (non-empty-listof real?) (values real? real?))
  (on-cdrs (let-values ([(mx mn) rec])
             (values (max mx it) (min mn it)))
           (values (car args) (car args))
           (cdr args)))

(our-length '(1 2 3 4))
(maxmin '(1 2 5 3 4))
1 Like

Yes, this is key!

Nit: You should use (make-variable-like-transformer #'(gfn)) here so that this expression:

((on-cdrs (rec it)
          (let loop ([x 0])
            (case-lambda
              [()
               x]
              [(y)
               (loop (+ x y))]))
          '(3 2 1 4)))

evaluates to 10, not 0.

Complaint for Paul Graham (unless I badly misunderstand the Common Lisp semantics): on-cdrs is written to accept zero or more lsts expressions, but an alrec expression evaluates to a function that accepts exactly one list.


An idiomatic Racket way to write maxmin might be something like this:

(define maxmin
  (match-lambda
    [(cons arg0 arg*)
     (for/fold ([mx arg0]
                [mn arg0])
               ([it (in-list arg*)])
       (values (max mx it)
               (min mn it)))]))

From a macro design perspective, a notable difference between for/fold and on-cdrs is that, while both allow working with an arbitrary number of values, for/fold makes the number of values in a particular for/fold expression statically visible, whereas, with on-cdrs, the macro implementation has to handle expressions that produce an unknown number of values. In Racket, knowing the number of values is preferable for style and efficiency. The considerations may way differently in Common Lisp, given e.g. the semantics for coercing multiple values returned to a single-value-expecting continuation.

1 Like

I'll have to look into that one. Never heard of it before.

I didn't dig deeply enough into them to notice that oversight. Weird.

Hello.

Hmm...My Racket(8.13) does not recognize make-rename-transformer, require-ing it though.
Let me take a while to study around it.

Thanks.

Maybe you need an (require (for-syntax ...)) around it.

Hello.

Yes, of course I did.
I might make a mistake...
Well, whatever, I need to study around it , taking a while.

Thanks.

It's equally possible to do this correctly "by hand" (the whole implementation is only 23 lines), but make-variable-like-transformer takes care of subtleties for robustness, like adding #%expression, that are easy to forget.