Writing myletstar macro (hygiene) in Scheme

146 Views Asked by At

I am trying to re-write let* hygiene macro, I have it as normal macro and I would like to have it as hygiene macro if it's possible. I don't have too much experience with this macro types. So I would really appreciate help. Also my another representation of working let* macro is not working, with same error as hygiene macro.

Working let*

(define-macro let*1
(lambda (assgn . body)
(let ((loop (gensym))
      (lst (gensym)))
  (let loop ((lst assgn))
     (if (null? lst)
         `(begin ,@body)
         `((lambda (,(caar lst))
             ,(loop (cdr lst)))
           ,(cadar lst)))))))

Not working hygiene let* -> error: lambda: not an identifier in: (caar lst)

(define-syntax let*2
(syntax-rules ()
((let*2 (set ...) body ...)
 (let loop ((lst '(set ...)))
   (if (null? lst)
       body ...
       ((lambda ((caar lst))
          (loop (cdr lst)))
        (cadar lst) 1))))))

Not working let*, but also having same error as the second one.

(define-macro let*3
(lambda (assgn . body)
(let ((loop (gensym))
      (lst (gensym)))
  `(let ,loop ((,lst assgn))
     (if (null? ,lst)
         (begin ,@body)
         ((lambda ((caar ,lst))
             (,loop (cdr ,lst)))
           (cadar ,lst)))))))

Apologize for little confusing question, I am stucked with this problem for some time already and caffeine isn't usefull anymore.

Some tests (I picked symbol names to test symbol-capture(I know I didn't had to)):

(let*1 ((body 10)
   (lst (+ body 1)))
  (list body lst))

(let*2 ((body 10)
    (lst (+ body 1)))
  (list body lst))

(let*3 ((body 10)
    (lst (+ body 1)))
  (list body lst))

Edit: Question is answered, added solution without using let by editing Lief Andersen code

(define-syntax let*
  (syntax-rules ()
    ((let*2 ([x1 e1][x2 e2] ...)body ...)
     ((lambda  (x1)
        (let* ([x2 e2] ...)
           body ...))
         e1))))
3

There are 3 best solutions below

2
On BEST ANSWER

Your second one is the closest. A hygenic let* macro can be written with define-syntax-rule. (If you're writing this in an implementation of scheme rather than racket, you can also just compose define-syntax and syntax-rule for the same effect.)

(define-syntax-rule (let* ([x1 e1]
                           [x2 e2] ...)
                      body ...)
  (let ([x1 e1])
    (let* ([x2 e2] ...)
      body ...))

And if you're feeling really pedantic, you can make a hygenic let macro:

(define-syntax-rule (let ([x e] ...)
                      body ...)
  ((lambda (x ...) body ...) e ...))
1
On

R6RS, Appendix B gives the following definition of let*:

(define-syntax let*
  (syntax-rules ()
    ((let* () body1 body2 ...)
     (let () body1 body2 ...))
    ((let* ((name1 expr1) (name2 expr2) ...)
       body1 body2 ...)
     (let ((name1 expr1))
       (let* ((name2 expr2) ...)
         body1 body2 ...)))))
2
On

When it comes to let* I don't think hygiene is an issue. The names in the let you want to expose and you don't introduce any other bindings.

(require compatibility/defmacro)
(define-macro let*1
  (lambda (assgn . body)
    (let loop ((lst assgn))
      (if (null? lst)
          `(begin ,@body)
          `((lambda (,(caar lst))
              ,(loop (cdr lst)))
            ,(cadar lst))))))

(expand-once 
 #'(let*1 
     ((a 1) (b 2) (c 3)) 
       (list a b c)))

; ==> #'((lambda (a)
;          ((lambda (b)
;            ((lambda (c)
;               (begin
;                 (list a b c)))
;             3))
;          2))
;       1)

Seems ok for me. Now you could do the same using syntax-rules. This method doesn't use scheme language and thus you trying to do (caar lst) is assumed you want that in the resulting expansion.

(expand-once #'(let*2 ((a 1) (b 2) (c 3)) (list a b c)))
; ==> #'(let loop ((lst '((a 1) (b 2) (c 3))))
;         (if (null? lst)
;             (list a b c)
;             ((lambda ((caar lst))
;                (loop (cdr lst)))
;              (cadar lst)
;              1)))

Notice how your code actually is copied verbatim into the resulting expandion. It's because everything that is not destructured in the pattern is assumed to be in the lexical scope of the syntax itself. Here is how to do it:

(define-syntax let*2
  (syntax-rules ()
    ;; handle stop condition (no more bindings)
    ((let*2 ()  body ...) 
     (begin body ...))
    ;; handle one expansion
    ((let*2 ((name expr) more-bindings ...) body ...)
     (let ((name expr))
       (let*2 (more-bindings ...)
         body ...))))) 
(expand-once 
 #'(let*2 
     ((a 1) (b 2) (c 3)) 
       (list a b c)))

; ==> #'((lambda (a) 
;          (let*2 ((b 2) (c 3)) 
;            (list a b c))) 1)

expand-once only does one level, but by using the macro expander you'll see it continues until all parts are expanded.

So when do you need hygiene? It's when you introduce bindings in your macro. The standard example is usually swap! that swaps the values of two bindings:

(define-syntax swap!
  (syntax-rules ()
    ((swap! var1 var2)
     (let ((tmp var1))
       (set! var1 var2)
       (set! var2 tmp)))))

With syntax-rules every binding not a pattern is though to be in the lexical scope of the macro and eveything not is a new binding that gets created new at each expansion. Thus I can safely do this:

(let ((tmp 1) (another 2))
  (swap! tmp another)
  (list tmp another))
; ==> (2 1)

With the old style you'd need this:

(define-macro swap!
  (lambda (var1 var2)
    (let ((tmp (gensym "tmp")))
      `(let ((,tmp ,var1))
         (set! ,var1 ,var2)
         (set! ,var2 ,tmp)))))

This works for the example above, but if I were to do:

(let ((set! 1) (let 2))
  (swap! set! let)
  (list set! let))

The syntax-rules one will evaluate to (2 1) while the define-macro one won't work.