I was curious about defining multiple lexically scoped functions in Scheme that can call each other. Working in SICP, I produced the following function using block structure to solve Exercise 1.8 (calculating cube-root using Newton’s method):
(define (cbrt x)
(define (good-enough? guess prev-guess)
(< (/ (abs (- guess prev-guess))
guess)
0.001))
(define (improve guess)
(/ (+ (/ x (square guess))
(* 2 guess))
3))
(define (cbrt-iter guess prev-guess)
(if (good-enough? guess prev-guess)
guess
(cbrt-iter (improve guess)
guess)))
(cbrt-iter 1.0 0.0))
This works fine, but it got me wondering how Scheme (and perhaps Common Lisp) might handle this same scenario using lexical scoping and the let form. I tried to implement it using let with the following kludgy code:
(define (cbrt x)
(let ((calc-cbrt
(lambda (guess prev-guess)
(let ((good-enough?
(lambda (guess prev-guess)
(< (/ (abs (- guess prev-guess))
guess)
0.001))))
(good-enough? guess prev-guess))
(let ((improve
(lambda (guess)
(/ (+ (/ x (square guess))
(* 2 guess))
3))))
(improve guess))
(let ((cbrt-iter
(lambda (guess prev-guess)
(if (good-enough? guess prev-guess)
guess
(cbrt-iter (improve guess)
guess)))))
(cbrt-iter 1.0 0.0)))))
(calc-cbrt 1.0 0.0)))
The problem that I see below is when cbrt-iter attempts to call the good-enough? procedure. Since the good-enough? procedure is only local to the scope of the first nested let block, cbrt-iter has no way to access it. It seems that this can be solved by nesting the cbrt-iter function within the enclosing let of good-enough, but this seems also very kludgy and awkward.
What is the define form doing that is different in this case? Is the define form expanding to lambda expressions instead of the “let over lambda” form (I recall something similar being done in the Little Schemer book using the form ((lambda (x) x x) (lambda (y) ...)), but I am not sure how this would work). Also, by way of comparison, how does Common Lisp handle this situation – is it possible to use lexically scoped defun‘s ?
First of all, you don’t need to introduce a new procedure
calc-cbrt– you could just callcalc-iterinstead.Second, the meaning of
defineandletare quite different.Defineinstalls the definitions into the local scope, as in your example. However,letexpressions are just syntactic sugar forlambdaexpressions (see SICP section 1.3 for details). As a result (and as you mention), variables declared via(let (<decl1> ...) <body>)are only visible inside<body>. So, your pattern of(let <decls1> <body1>) (let <decls2> <body2>) ...doesn’t work, since none of the definitions will “survive” to be seen in other scopes.So, we should write something like this:
Now, at least, the call to
cbrt-itercan see the definition ofcbrt-iter.But there’s still a problem. When we evaluate
(cbrt-iter 1.0 0.0), we evaluate the body ofcbrt-iterwhereguessandprev-guesstake the values 1.0 and 0.0. But, in the body ofcbrt-iter, the variablesimproveandgood-enough?aren’t in scope.You might be tempted to use nested
lets, which is often a good choice:The problem is that cbrt-iter needs to call itself, but it’s not in scope until the body of the inner
let!The solution here is to use
letrec, which is likeletbut makes the new bindings visible inside all the declarations as well as the body:We can even use
letrecto create mutually recursive procedures, just as we could withdefine.Unfortunately, it would take me some time to explain how
letrecanddefineactually work, but here’s the secret: they both use mutation internally to create circularity in the environment data structure, allowing recursion. (There is also a way to create recursion using onlylambda, called the Y combinator, but it’s rather convoluted and inefficient.)Luckily, all these secrets will be revealed in Chapter 3 and Chapter 4!
For another perspective, you might take a look at Brown University’s online PL class, which basically goes straight to this topic (although it omits
define), but I find that SICP is better at forcing you to understand the sometimes complex environment structures that are created.