tags:

views:

177

answers:

3

I am currently working through SICP using Guile as my primary language for the exercises. I have found a strange behavior while implementing the exercises in chapter 3.5. I have reproduced this behavior using Guile 1.4, Guile 1.8.6 and Guile 1.8.7 on a variety of platforms and am certain it is not specific to my setup.

This code works fine (and computes e):

  (define y (integral (delay dy) 1 0.001))
  (define dy (stream-map (lambda (x) x) y))
  (stream-ref y 1000)

The following code should give an identical result:

  (define (solve f y0 dt)
    (define y (integral (delay dy) y0 dt))
    (define dy (stream-map f y))
    y)
  (stream-ref (solve (lambda (x) x) 1 0.001) 1000)

But it yields the error message:

standard input:7:14: While evaluating arguments to stream-map in expression (stream-map f y):
standard input:7:14: Unbound variable:
y ABORT: (unbound-variable)

So when embedded in a procedure definition, the (define y ...) does not work, whereas outside the procedure in the global environment at the REPL it works fine.

What am I doing wrong here? I can post the auxiliary code (i.e., the definitions of integral, stream-map etc.) if necessary, too. With the exception of the system-dependent code for cons-stream, they are all in the book. My own implementation of cons-stream for Guile is as follows:

(define-macro (cons-stream a b)
  `(cons ,a (delay ,b)))
+1  A: 

The key difference between what happens when you evaluate the definitions one by one at the REPL and when you place them inside solve is that in the first case, they are evaluated sequentially, thus the y the expression (stream-map <some-function> y) refers to is already in scope, whereas with internal definitions or letrec, it is not yet available.

Funnily enough, MIT Scheme, which I used when going through SICP, had no such problem back then and still treats letrec and internal defines differently:

;; this is an error
(letrec ((xs '(1 2 3)) (ys (map (lambda (x) (+ x 1)) xs))) ys)

;; this is still an error (and is treated as such by Guile),
;; yet evaluates to (2 3 4) in MIT Scheme
(let () (define xs '(1 2 3)) (define ys (map (lambda (x) (+ x 1)) xs)) ys)

I'm not sure about the original "Revised Report On Algorithmic Language Scheme" or R2RS, but at least from R3RS on internal defines were supposed to be equivalent to letrec. Apparently this peculiarity of MIT's environment influenced the book... or perhaps it's the other way around.

Michał Marczyk
Then why would I want to use internal defines at all? The restriction mentioned in R5RS (see my comment above) to allow defines only at the beginning of a <body> would be pointless if they were unavailable for the rest of the body.Even worse: When I type in the two blocks of expressions quoted in my post *in sequence* (i.e., with "y" and "dy" already defined in the global environment), I still get the same error message when evaluating (stream-ref (solve ...) 1000), even though Scheme is supposed to refer to the one in the global environment when the local one is unavailable.
user8472
Scheme is *not* supposed to refer to the global binding; `letrec` bindings (and internal defines, which are equivalent) are meant to be mutually recursive. *However*, quoting R5RS: "Just as for the equivalent letrec expression, it must be possible to evaluate each <expression> of every internal definition in a <body> without assigning or referring to the value of any <variable> being defined." There are all sorts of reasons why `letrec` expressions are useful and internal defines, being just an alternate syntax for `letrec`, are useful in the same situations.
Michał Marczyk
Anyway, as far as I can tell, this is what goes wrong with the code at hand. You could try to fix things with `delay` / `force`. The rationale behind R5RS' mandating such behaviour is a different matter, which I'm not sure I'm prepared to discuss extempore.
Michał Marczyk
Oh, one more thing. If you're willing to solve the exercises with just the tools presented earlier in the book -- an approach I was personally very happy with -- then I think MIT Scheme might be the best environment to do it in. It'll certainly save you from having to worry about the code from the book not executing correctly and the educational benefit will be undiminished. There are also some SICP support packages on PLaneT, which likely provide a similar experience (though I haven't tested them myself). Of course smoothing over some edges with another impl may be a good exercise in itself.
Michał Marczyk
One benefit for me certainly was that I had to learn the basics of Lisp macros in order to implement the "cons-stream" macro. Whether smoothing edges is educationally beneficial or not is certainly a matter of debate. However, having to do so is quite common in real-life software development, so figuring out who is "at fault" has some practical uses ;^)
user8472
+1  A: 

You can't have internal DEFINEs that depend on one another; the language spec explicitly states this (R5RS 5.2.2):

... it must be possible to evaluate each expression of every internal definition in a body without assigning or referring to the value of any variable being defined.

You can think of this as though the interpreter is collecting all the DEFINES and evaluating them prior to the body in a random order. Because the order is random, there can't be any interdependencies if you expect it to work.

There's even a footnote attached to the SOLVE definition (#71) that says it's not going to work on all Schemes.

You have to write the code so that one definition is very clearly in the scope of the other, like with nested LETs:

(define (solve f y0 dt)
  (let ((y (integral (delay dy) y0 dt)))
    (let ((dy (stream-map f y)))
      y)))
Cirno de Bergerac
Except the `dy` in `(delay dy)` is supposed to refer to the `dy` defined below... Careful use of `delay` together with a flat `letrec` is likely to be required. Good catch on the footnote, though.
Michał Marczyk
Yes, thanks for pointing out the footnote. There is one similar comment in R5RS 4.2.2: "One restriction on `letrec' is very important: it must be possible to evaluate each <init> without assigning or referring to the value of any <variable>. If this restriction is violated, then it is an error." The circular definition certainly refers to a *value* of a variable. So is it possible that such a circular definition is not valid Scheme in the first place? If so, wrapping the code in procedures via (lambda () ...) might be the "correct" solution.
user8472
A: 

Following the idea in the comments (referring to the quote from R5RS 4.2.2) I have now wrapped the definitions of "y" and "dy" into (lambda () ...)s:

  (define (solve f y0 dt)
    (define (y) (integral (delay (dy)) y0 dt))
    (define (dy) (stream-map f (y)))
    (y))

This makes sure that that the <init> part of each definition can be evaluated without referring to the circular defined variables since the definitions are procedures rather than expressions with other variables like in the original case.

Now the code is certainly much slower (since the functions will get wrapped recursively) and the stack size needs to be increased, but the following works and produces the correct result:

  (debug-set! stack 2000000)
  (stream-ref (solve (lambda (x) x) 1 0.001) 1000)

With a similar modification, Michał's example code works as soon as one defines procedures rather than variables:

  (let ()
    (define (xs) '(1 2 3))
    (define (ys) (map (lambda (x) (+ x 1)) (xs)))
    (ys))

works on Guile 1.8.6.

user8472