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?