Search code examples
sicp

Moral of the story from SICP Ex. 1.20?


In this exercise we are asked to trace Euclid's algorithm using first normal order then applicative order evaluation.

(define (gcd a b)
  (if (= b 0)
      a
      (gcd b (remainder a b))))

I've done the manual trace, and checked it with the several solutions available on the internet. I'm curious here to consolidate the moral of the exercise.

In gcd above, note that b is re-used three times in the function body, plus this function is recursive. This being what gives rise to 18 calls to remainder for normal order, in contrast to only 4 for applicative order.

So, it seems that when a function uses an argument more than once in its body, (and perhaps recursively! as here), then not evaluating it when the function is called (i.e. applicative order), will lead to redundant recomputation of the same thing.

Note that the question is at pains to point out that the special form if does not change its behaviour when normal order is used; that is, if will always run first; if this didn't happen, there could be no termination in this example.

I'm curious regarding delayed evaluation we are seeing here.

As a plus, it might allow us to handle infinite things, like streams. As a minus, if we have a function like here, it can cause great inefficiency. To fix the latter it seems like there are two conceptual options. One, wrap it in some data structure that caches its result to avoid recomputation. Two, selectively force the argument to realise when you know it will otherwise cause repeated recomputation.

The thing is, neither of those options seem very nice, because both represent additional "levers" the programmer must know how to use and choose when to use.

Is all of this dealt with more thoroughly later in the book? Is there any straightforward consolidation of these points which would be worth making clear at this point (without perhaps going into all the detail that is to come).


Solution

  • The short answer is yes, this is covered extensively later in the book.

    It is covered in most detail in Chapter 4 where we implement eval and apply, and so get the opportunity to modify their behaviour. For example Exercise 4.31 suggests the following syntax:

    (define (f a (b lazy) c (d lazy-memo))

    As you can see this identifies three different behaviours.

    • Parameters a and c have applicative order (they are evaluated before they are passed to the f).
    • b is normal or lazy, it is evaluated everytime it is used (but only if it is used).
    • d lazy but it's value it memoized so it is evaluated at most once.

    Although this syntax is possible it isn't used. I think the philosopy is that the the language has an expected behaviour (applicative order) and that normal order is only used by default when necessary (e.g., the consequent and alternative of an if statement, and in creating streams). When it is necssary (or desirable) to have a parameter with normal evaluation then we can use delay and force, and if necessary memo-proc (e.g. Exercise 3.77).

    So at this point the authors are introducing the ideas of normal and applicative order so that we have some familiarity with them by the time we get into the nitty gritty later on.

    In a sense this a recurring theme, applicative order is probably more intuitive, but sometimes we need normal order. Recursive functions are simpler to write, but sometimes we need the performance of iterative functions. Programs where we can use the substition model are easier to reason about, but sometimes we need environmental model because we need mutable state.