Is there a way to implement named let as macro to make it work with Petrofsky let

62 Views Asked by At

I have Scheme interpreter written in JavaScript. And named let macro expand into letrec with lambda expression. But the problem is the Petrofsky let

(test.failing "std: Petrofsky let"
      (lambda (t)
        (t.is (let - ((n (- 1))) n) -1)))

It expands into:

(letrec ((- (lambda (n) n))) (- (- 1)))

Is there a way to implement named let as scheme code that will work for Petrofsky let and return -1? Or do I need to implement named let as a basic construct in JavaScript?

I will probably implement it in JavaScript, because named let was kind of quick hack added without thinking. But I still want to know if it's possible to do this in Scheme.

3

There are 3 best solutions below

0
jcubic On

I came up with this implementation:

(let ((#:args (list (- 1))))
  (letrec ((- (lambda (n) n)))
    (apply - #:args)))

Where #:args is unique value that can't appear inside user code. In my case, it was a gensym.

7
Shawn On

One way to implement named let without using letrec or an equivalent internal define is to use a Y-combinator. Example using a syntax-rules transformer:

(define-syntax my-named-let
  (syntax-rules ()
    ((_ name ((id init) ...) body0 body1 ...)
     (((lambda (h)
         ((lambda (x) (h (lambda a (apply (x x) a))))
          (lambda (x) (h (lambda a (apply (x x) a))))))
       (lambda (name) (lambda (id ...) body0 body1 ...))) init ...))))

I only saw define-macro listed in your documentation, though. Here's a traditional Lisp-style macro version (Tested with Guile and Gauche and also on LIPS):

(define-macro (my-named-let name ids-and-inits . body)
  `(((lambda (h)
       ((lambda (x) (h (lambda a (apply (x x) a))))
        (lambda (x) (h (lambda a (apply (x x) a))))))
     (lambda (,name) (lambda ,(map car ids-and-inits) ,@body)))
    ,@(map cadr ids-and-inits)))

A less esoteric approach is to initially bind all the initializers to temporary variables outside of the letrec and then use those variables for the initial call:

(define-macro (my-named-let name ids-and-inits . body)
  (let ((temp-ids (map (lambda (id) (gensym)) ids-and-inits)))
    `(let ,(map (lambda (temp-id id-and-init) (list temp-id (cadr id-and-init)))
                temp-ids
                ids-and-inits)
       (letrec ((,name (lambda ,(map car ids-and-inits) ,@body)))
         (,name ,@temp-ids)))))

With either macro, you should get the expected -1 from your test case.

Distinguishing a named let from a regular let in the same macro is left as an exercise for the reader.


A version based on the syntax-rules implementation given in R7RS, that uses letrec to return the lambda, and then calls that, instead of having the initial call inside the scope of the letrec, meaning you don't need the gensym'ed temporary variables. This version also supports regular nameless let in the same macro:

(define-macro (let name-or-bindings body-or-bindings . body)
  (cond
   ((symbol? name-or-bindings)
    `((letrec ((,name-or-bindings (lambda ,(map car body-or-bindings) ,@body)))
        ,name-or-bindings)
      ,@(map cadr body-or-bindings)))
   ((pair? name-or-bindings)
    `((lambda ,(map car name-or-bindings) ,body-or-bindings ,@body)
      ,@(map cadr name-or-bindings)))
   (else (error "invalid syntax for let"))))
2
ignis volens On

Here's a version which uses neither something like Y nor a bunch of temporary variables, but does use assignment. The trick is the same one implementations of letrec do: bind the function's name to something temporary, then make it be the actual function.

In other words you want

(nlet - ((x (- 1))) x)

to turn into

(let ((- #f) (x (- 1)))
  (set! - (lambda (x) x))
  (- x))

This may be incorrect in some way I have not thought of, but I think it's OK.

Here's a syntax-rules implementation:

(define-syntax nlet
  (syntax-rules ()
    ((_ name ((var init) ...) form ...)
     (let ((name #f) (var init) ...)
       (set! name (lambda (var ...) form ...))
       (name var ...)))))

Here's a traditional-lisp-macro one:

(define-macro (nlet name bindings . forms)
  (let ((vars (map car bindings)))
    `(let ((,name #f) ,@bindings)
     (set! ,name (lambda ,vars ,@forms))
     (,name ,@vars))))

Both should have better error detection.