Search code examples
functional-programmingschemesicp

SICP 3.6 - Rand procedure and local state variables


I'm having difficulties with exercise 3.6 in SICP. They give the following code for a pseudo-random number generator:

(define rand
  (let ((x random-init))
    (lambda ()
      (set! x (rand-update x))
      x)))

To which I added, for testing purposes:

(define (rand-update x) (+ x 1))
(define random-init 4)

Repeated applications produces

> (rand)
5
> (rand)
6
> (rand)
7

as hoped, although I can't see why this works. Anyway, exercise 3.6 asks us to modify rand so that it takes a single argument, instructing it to either 'generate or 'reset.

First things first, I tried to set up a rand with conditions that would generate. However, I stumbled at this first hurdle.

(define (rand-new instruction)
  (let ((x random-init))
    (cond ((eq? instruction 'generate)
          (lambda ()
            (set! x (rand-update x))
            x)))))

gives me

> ((rand-new 'generate))
5
> ((rand-new 'generate))
5
> ((rand-new 'generate))
5

as does moving the let expression into the conidition, like this:

(define (rand-new instruction)
  (cond ((eq? instruction 'generate)
         (let ((x random-init))
           (lambda ()
             (set! x (rand-update x))
             x)))))

So why does the first function work, and the new one doesn't? Is it to do with the use of conditions? Or with the addition of an parameter?

UPDATE (19/03/20)

Reading the next section on the environment model of computation (§3.2) gave me the theory necessary to figure out what was going on. I ended up with

(define (random-number-generator initial update)
  (define (generate)
    (begin (set! initial (update initial))
          initial))
  (define (reset new-value)
    (begin (set! initial new-value)
           initial))
  (define (dispatch message)
    (cond ((eq? message 'generate) (generate))
          ((eq? message 'reset) reset)
          (else "Procedure not found!")))
  dispatch)

(define rand (random-number-generator 5 rand-update))

Solution

  • The key point for understanding why the first version works (and why the other doesn't) is in the first three lines:

    (define rand
      (let ((x random-init))
        (lambda ()
    

    As you can see, the name rand is assigned to a lambda - but before doing that a variable x is created in a scope outside of the lambda, meaning: that no matter how many times we call rand, the value in x will "remember" its previous value, which we set in the previous call: (set! x (rand-update x)).

    So, you must respect the placement of that let and that lambda, otherwise the procedure you create won't have any "memory" between invocations. Besides, I don't think the exercise is asking you to create your own random procedure, it'd be enough to create a wrapper around the built-in procedures that accepts the required messages:

    (define (make-rand)
      (λ (msg)
        (case msg
          ('reset (λ (seed) (random-seed seed)))
          ('generate (random)))))
    
    (define rand (make-rand))
    

    For example:

    ((rand 'reset) 42)
    (rand 'generate)
    => 0.07337258110323383
    (rand 'generate)
    => 0.0887121382290788
    ((rand 'reset) 42)
    (rand 'generate)
    => 0.07337258110323383
    (rand 'generate)
    => 0.0887121382290788
    

    If you decide to implement your own version of random, try to keep the procedures separate (as I did above), if you put everything in a single place things will get confusing pretty soon.