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:
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)
(make-stream
(lambda ()
(if ($null? a)
b
($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)))
val)))))
(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)
(make-stream
(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 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
What about assignment? To support this we must change the form of judgments
to use and produce a store. Now we have 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.
Constants
-----
p |- #n => n
Since constants have no subexpressions, there are no antecedents. The
line above is often left out.
Variables
e(x) = v
--------
p |- x => v
Lambda
--------------------------------
p |- (lambda (x) e) => [p, x, e]
Here the object [p, x, e] is a closure.
Application
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).
Begin
p |- e[1] => v ... p |- e[n] => v[n]
----------------------------------------
p |- (begin e[1] ... e[n]) => v[n]
Let
p |- e[1] => v[1] p[x |-> v] |- e[2] => v[2]
------------------------------------------------
p |- (let ((x e[1])) e[2]) => v[2]
Exercise
Construct a proof that (let ((f (lambda (x) (succ x)))) (begin (f 1) (f 2)))
evaluates to 3.
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.
**Puzzle
Is it possible to describe let/cc using natural semantics?