3
votes

I feel that understanding this subtlety might help me to understand how scope works in Scheme.

So why does Scheme error out if you try to do something like:

(define (func n)
  (define n (+ 1 n))
  n)

It only errors out at runtime when calling the function.

The reason I find it strange is because Scheme does allow re-definitions, even inside functions. For example this gives no error and will always return the value 5 as expected:

(define (func n)
  (define n 5)
  n)

Additionally, Scheme also seems to support self-re-definition in global space. For instance:

(define a 5)
(define a (+ 1 a))

gives no error and results in "a" displaying "6" as expected.

So why does the same thing give a runtime error when it occurs inside a function (which does support re-definition)? In other words: Why does self-re-definition only not work when inside of a function?

1

1 Answers

3
votes

global define

First off, top level programs are handled by a different part of the implementation than in a function and defining an already defined variable is not allowed.

(define a 10)
(define a 20) ; ERROR: duplicate definition for identifier

It might happen that it works in a REPL since it's common to redefine stuff, but when running programs this is absolutely forbidden. In R5RS and before what happened is underspecified and didn't care since be specification by violating it it no longer is a Scheme program and implementers are free to do whatever they want. The result is of course that a lot of underspecified stuff gets implementation specific behaviour which are not portable or stable.

Solution:

set! mutates bindings:

(define a 10)
(set! a 20) 

define in a lambda (function, let, ...)

A define in a lambda is something completely different, handled by completely different parts of the implementation. It is handled by the macro/special form lambda so that it is rewritten to a letrec*

A letrec* or letrec is used for making functions recursive so the names need to be available at the time the expressions are evaluated. Because of that when you define n it has already shadowed the n you passed as argument. In addition from R6RS implementers are required to signal an error when a binding is evaluated that is not yet initialized and that is probably what happens. Before R6RS implementers were free to do whatever they wanted:

(define (func n)
  (define n (+ n 1)) ; illegal since n hasn't got a value yet!
  n)

This actually becomes:

(define func
  (lambda (n)
    (let ((n 'undefined-blow-up-if-evaluated))
      (let ((tmpn (+ n 1)))
        (set! n tmpn))
      n)))

Now a compiler might see that it violates the spec at compile time, but many implementations doesn't know before it runs.

(func 5) ; ==> 42

Perfectly fine result in R5RS if the implementers have good taste in books. The difference in the version you said works is that this does not violate the rule of evaluating n before the body:

(define (func n)
  (define n 5)
  n)

becomes:

(define func
  (lambda (n)
    (let ((n 'undefined-blow-up-if-evaluated))
      (let ((tmpn 5)) ; n is never evaluated here!
        (set! n tmpn))
      n)))

Solutions

Use a non conflicting name

(define (func n)
  (define new-n (+ n 1))
  new-n)

Use let. It does not have its own binding when the expression gets evaluated:

(define (func n)
  (let ((n (+ n 1)))
    n))