COS 441 - Streams and Natural Semantics - April 4, 1996


A stream is a potentially infinite lazy list. It evaluates its cdr and maybe also its car only when demanded. It supports the same operations as lists, with one additional operation:

The prefix $ indicates stream. The function make-stream takes a thunk and returns a stream. We can implement these functions using memoization as follows.
(define $cons cons)
(define $null? null?)
(define $nil '())

(define $car 
  (lambda (s)
    (if (pair? s) (car s) (car (s)))))
(define $cdr
  (lambda (s)
    (if (pair? s) (cdr s) (cdr (s)))))
(define $append
  (lambda (a b)
      (lambda ()
        (if ($null? a)
            ($cons ($car a) ($append ($cdr a) b)))))))
(define memo
  (lambda (p)
    (let ((first #t)
    	  (val #f))
      (lambda ()
        (if first
            (begin (set! first $f) (set! val (p)))
(define make-stream memo)
Recall the question the on the mid-term that asked you to write same-fringe?. We can do that by transforming a tree into a stream and calling $equal.
(define tree->stream
  (lambda (t)
      (lambda ()
        (if (symbol? t) 
            ($cons t $nil)
            ($append (tree->stream (car t)) (tree->stream (cdr t))))))))
(define $equal
  (lambda (s1 s2)
    (cond ((and ($null? s1) ($null? s2)) #t)
          ((or ($null? s1) ($null? s2)) #f)
          (else (and (eq? ($car s1) ($car s2))
                     ($equal ($cdr s1) ($cdr s2)))))))

(define same-fringe?
  (lambda (a b)
    ($equal (tree->stream a) (tree->stream b))))

Natural Semantics

Natural semantics describe the process of computation in a more structured manner than rewriting semantics. Natural semantics attempts to explain the behavior of an expression through the behavior of its subexpressions. For example:

if e[1] has value v[1] and
   e[2] has value v[2] and
   applying v[1] to v[2] gives v[3]
then (e[1] e[2]) has value v[3]
The parts above the dashed line are called antecedents. The parts below are called consequents. What happens if e[1], e[2] have free variables? We use an environment p to record bindings of free variables.

A judgment is of the form p |- e => v which may be read as "in environment p, expression e evaluates to value v." We formulate a rule for each form of expression.


p |- #n => n
Since constants have no subexpressions, there are no antecedents. The line above is often left out.


e(x) = v
p |- x => v


p |- (lambda (x) e) => [p, x, e]
Here the object [p, x, e] is a closure.


p |- e[1] => [p', x, e]    p |- e[2] => v[2]    p'[x |-> v] |- e => v[3]
p |- (e[1] e[2]) => v[3]
We can also have special functions here, such as successor (succ).
p |- e[1] => succ     p |- e[2] => n
p |- (e[1] e[2]) => n+1
A proof that p |- e => v is a tree constructed from these rules, ending (or rooted) in the judgment p |- e => v. For "wrong" expressions like (succ (lambda (x) x)) there is no such tree. For divergent expressions like omega, there is also no tree (it would be infinite).


p |- e[1] => v   ...   p |- e[n] => v[n]
p |- (begin e[1] ... e[n]) => v[n]


p |- e[1] => v[1]     p[x |-> v] |- e[2] => v[2]
p |- (let ((x e[1])) e[2]) => v[2]


Construct a proof that (let ((f (lambda (x) (succ x)))) (begin (f 1) (f 2))) evaluates to 3.

What about assignment? To support this we must change the form of judgments to use and produce a store. Now we have p, s |- e => v, s[2]. This means "starting with store s, e evaluates to v in environment p and yields store s[2]." Here are some of the new rules.

p, s |- e[1] => [p', x, e], s[2]    
p, s[2] |- e[2] => v[2], s[3]
p'[x |->v], s[3] |- e => v[3], s[4]
p, s[1] |- (e[1] e[2]) => v[3], s[4]

p, s[1] |- e => v, s[2]   (x in dom(s[2])
p, s[1] |- (set! x e) => #f, s[2][x |-> v]
You are now ready to read the dynamic semantics chapters of the definition of standard ML.


Is it possible to describe let/cc using natural semantics?