I have been working with lisp-family languages for several years and feel like I have a pretty good grasp on them. I'm now writing my own lisp (as is the fashion, of course), but almost entirely avoiding re-implementing the same patterns that Scheme, Common Lisp and friends have used. One particular thing that I always found odd was all the variants of let
(letrec
, flet
, labels
, let*
...).
Say in a "no legacy carried over from the past" implementation of a lisp, I'd like to just be able to write things like:
(let ((x 7)
(y 8)
(z (* x y)))
(str "x * y * z = " (* x y z)))
And similarly I'd like to be able to write:
(let ((odd? (lambda (n) (if (zero? n) false (even? (dec n)))))
(even? (lambda (n) (if (zero? n) true (odd? (dec n)))))
(odd? 7))
I could implement both of those variants quite efficiently by just using define inside the body of a lambda. Excuse the clojure-scheme-hybrid code:
(defmacro let (bindings & body)
`((lambda ()
,@(map (lambda (name-value) (cons 'define name-value)) bindings)
,@body)))
Because this just introduces the bindings within the closed environment in the body of the lambda, mutual recursion can work and variables can depend on the existence of previous definitions. Similarly, because a lambda closes the environment in which it is created, expanding the (define x expression) (define y expression)
works as expected.
So why complicate matters by having numerous explicit forms? Am I overlooking something? I have implement my own let
as I demonstrate above and it seems to work in all cases exactly as expected. This also reduces the cost overhead of nested function applications, such as in the case of let*
.