Tuesday, January 7th Intro to CS4400/CS5400
-
General plan for how the course will go.
-
Administrative stuff. (Mostly going over the web pages.)
https://pl.barzilay.org/
Tuesday, January 7th Intro to Programming Languages
-
Why should we care about programming languages? (Any examples of big projects without a little language?)
-
What defines a language?
- syntax
- semantics
- libraries (runtime)
- idioms (community)
-
How important is each of these?
-
libraries give you the run-time support, not an important part of the language itself. (BTW, the line between “a library” and “part of the language” is less obvious than it seems.)
-
idioms originate from both language design and culture. They are often misleading. For example, JavaScript programmers will often write:
function explorer_move() { doThis(); }
function mozilla_move() { doThat(); }
if (isExplorer)
document.onmousemove = explorer_move;
else
document.onmousemove = mozilla_move;or
if (isExplorer)
document.onmousemove = function() { doThis(); };
else
document.onmousemove = function() { doThat(); };or
document.onmousemove =
isExplorer ? function() { ... }
: function() { ... };or
document.onmousemove =
isExplorer ? () => { doThis(); } : () => { doThat(); };or
document.onmousemove = isExplorer ? doThis : doThat;How many JavaScript programmers will know what this does:
function foo(n) {
return function(m) { return m + n; };
}or this:
n => m => m+n;
(x,y) => s => s(x,y);or, what seems fishy in this? —
const foo = (x,y) => bar(x,y)Yet another example:
let x = "";
while (foo())
x += whatever();How would you expect this code perform? How do you think it does in the reality of many uses of JS by people who are not really programmers?
-
Compare:
-
a[25]+5
(Java: exception) -
(+ (vector-ref a 25) 5)
(Racket: exception) -
a[25]+5
(JavaScript: exception (or NaN)) -
a[25]+5
(Python: exception) -
$a[25]+5
(Perl: 5) -
a[25]+5
(C: BOOM) - syntax is mostly in the cosmetics department; semantics is the real thing.
-
-
Another example:
-
a + 1 > a
(Python: always true) -
(> (+ a 1) a)
(Racket: always true) -
a + 1 > a
(C: sometimes true)
-
-
-
How should we talk about semantics?
-
A few well-known formalisms for semantics.
-
We will use programs to explain semantics: the best explanation is a program.
-
Ignore possible philosophical issues with circularity (but be aware of them). (Actually, they are solved: Scheme has a formal explanation that can be taken as a translation from Scheme to logic, which means that things that we write can be translated to logic.)
-
We will use Racket for many reasons (syntax, functional, practical, simple, formal, statically typed, environment).
-
Tuesday, January 7th Intro to Racket
-
General layout of the parts of Racket:
-
The Racket language is (mostly) in the Scheme family, or more generally in the Lisp family;
-
Racket: the core language implementation (language and runtime), written in C;
-
The actual language(s) that are available in Racket have lots of additional parts that are implemented in Racket itself;
-
GRacket: a portable Racket GUI extension, written in Racket too;
-
DrRacket: a GRacket application (also written in Racket);
-
Our language(s)…
-
-
Documentation: the Racket documentation is your friend (But beware that some things are provided in different forms from different places).
Tuesday, January 7th Side-note: “Goto Statement Considered Harmful”
A review of “Goto Statement Considered Harmful”, by E.W. DIJKSTRA
This paper tries to convince us that the well-known goto statement should be eliminated from our programming languages or, at least (since I don’t think that it will ever be eliminated), that programmers should not use it. It is not clear what should replace it. The paper doesn’t explain to us what would be the use of the
if
statement without agoto
to redirect the flow of execution: Should all our postconditions consist of a single statement, or should we only use the arithmeticif
, which doesn’t contain the offensivegoto
?And how will one deal with the case in which, having reached the end of an alternative, the program needs to continue the execution somewhere else?
The author is a proponent of the so-called “structured programming” style, in which, if I get it right, gotos are replaced by indentation. Structured programming is a nice academic exercise, which works well for small examples, but I doubt that any real-world program will ever be written in such a style. More than 10 years of industrial experience with Fortran have proved conclusively to everybody concerned that, in the real world, the goto is useful and necessary: its presence might cause some inconveniences in debugging, but it is a de facto standard and we must live with it. It will take more than the academic elucubrations of a purist to remove it from our languages.
Publishing this would waste valuable paper: Should it be published, I am as sure it will go uncited and unnoticed as I am confident that, 30 years from now, the goto will still be alive and well and used as widely as it is today.
Confidential comments to the editor: The author should withdraw the paper and submit it someplace where it will not be peer reviewed. A letter to the editor would be a perfect choice: Nobody will notice it there!
Tuesday, January 7th Quick Intro to Racket
Racket syntax: similar to other Sexpr-based languages.
Reminder: the parens can be compared to C/etc function call parens —
they always mean that some function is applied. This is the reason why
(+ (1) (2))
won’t work: if you use C syntax that is +(1(), 2())
but
1
isn’t a function so 1()
is an error.
An important difference between syntax and semantics: A good way to think about this is the difference between the string
42
stored in a file somewhere (two ASCII values), and the number42
stored in memory (in some representation). You could also continue with the above example: there is nothing wrong with “murder” — it’s just a word, but murder is something you’ll go to jail for.
The evaluation function that Racket uses is actually a function that takes a piece of syntax and returns (or executes) its semantics.
define
expressions are used for creating new bindings, do not try to
use them to change values. For example, you should not try to write
something like (define x (+ x 1))
in an attempt to mimic x = x+1
. It
will not work.
There are two boolean values built in to Racket: #t
(true) and #f
(false). They can be used in if
statements, for example:
because (< 2 3)
evaluates to #t
. As a matter of fact, any value
except for #f
is considered to be true, so:
(if "false" 1 2) --> 1
(if "" 1 2) --> 1
(if null 1 2) --> 1
(if #t 1 2) --> 1 ; including the true value
(if #f 1 2) --> 2 ; the only false value
(if #false 1 2) --> 2 ; another way to write it
(if false 1 2) --> 2 ; also false since it's bound to #f
Note: Racket is a functional language — so everything has a value.
This means that the expression
has no meaning when test
evaluates to #f
. This is unlike Pascal/C
where statements do something (side effect) like printing or an
assignment — here an if
statement with no alternate part will just
do nothing if the test is false… Racket, however, must return some
value — it could decide on simply returning #f
(or some unspecified
value) as the value of
as some implementations do, but Racket just declares it a syntax error.
(As we will see in the future, Racket has a more convenient when
with
a clearer intention.)
Well, almost everything is a value…
There are certain things that are part of Racket’s syntax — for
example if
and define
are special forms, they do not have a value!
More about this shortly.
(Bottom line: much more things do have a value, compared with other languages.)
cond
is used for a if
… else if
… else if
… else
… sequence.
The problem is that nested if
s are inconvenient. For example,
(if (<= n 9)
1
(if (<= n 99)
2
(if (<= n 999)
3
(if (<= n 9999)
4
"a lot")))))
In C/Java/Whatever, you’d write:
if (n <= 9) return 1;
else if (n <= 99) return 2;
else if (n <= 999) return 3;
else if (n <= 9999) return 4;
else return "a lot";
}
(Side question: why isn’t there a return
statement in Racket?)
But trying to force Racket code to look similar:
(if (<= n 9)
1
(if (<= n 99)
2
(if (<= n 999)
3
(if (<= n 9999)
4
"a lot")))))
is more than just bad taste — the indentation rules are there for a reason, the main one is that you can see the structure of your program at a quick glance, and this is no longer true in the above code. (Such code will be penalized!)
So, instead of this, we can use Racket’s cond
statement, like this:
(cond [(<= n 9) 1]
[(<= n 99) 2]
[(<= n 999) 3]
[(<= n 9999) 4]
[else "a lot"]))
Note that else
is a keyword that is used by the cond
form — you
should always use an else
clause (for similar reasons as an if
, to
avoid an extra expression evaluation there, and we will need it when we
use a typed language). Also note that square brackets are read by
DrRacket like round parens, it will only make sure that the paren pairs
match. We use this to make code more readable — specifically, there is
a major difference between the above use of []
from the conventional
use of ()
. Can you see what it is?
The general structure of a cond
:
[test-2 expr-2]
...
[test-n expr-n]
[else else-expr])
Example for using an if
expression, and a recursive function:
(if (zero? n)
1
(* n (fact (- n 1)))))
Use this to show the different tools, especially:
- special objects that cannot be used
- syntax-checker
- stepper
- submission tool (installing, registering and submitting)
An example of converting it to tail recursive form:
(if (zero? n)
acc
(helper (- n 1) (* acc n))))
(define (fact n)
(helper n 1))
Additional notes about homework submissions:
-
Begin every function with clear documentation: a type followed by a purpose statement.
-
Document the function when needed, and according to the guidelines above and in the style guide.
-
After the function, always have a few test cases — they should cover your complete code (make sure to include possible corner cases). Later on, we will switch to testing the whole file through it’s “public interface”, instead of testing each function.
Tuesday, January 7th Lists & Recursion
Lists are a fundamental Racket data type.
A list is defined as either:
-
the empty list (
null
,empty
, or'()
), -
a pair (
cons
cell) of anything and a list.
As simple as this may seem, it gives us precise formal rules to prove that something is a list.
- Why is there a “the” in the first rule?
Examples:
(cons 1 null)
(cons 1 (cons 2 (cons 3 null)))
(list 1 2 3) ; a more convenient function to get the above
List operations — predicates:
pair? ; true for any cons cell
list? ; this can be defined using the above
We can derive list?
from the above rules:
(if (null? x)
#t
(and (pair? x) (list? (rest x)))))
or better:
(or (null? x)
(and (pair? x) (list? (rest x)))))
But why can’t we define list?
more simply as
(or (null? x) (pair? x)))
The difference between the above definition and the proper one can be observed in the full Racket language, not in the student languages (where there are no pairs with non-list values in their tails).
List operations — destructors for pairs (cons
cells):
rest
Traditionally called car
, cdr
.
Also, any c<x>r
combination for <x>
that is made of up to four a
s
and/or d
s — we will probably not use much more than cadr
, caddr
etc.
Example for recursive function involving lists:
(if (null? list)
0
(+ 1 (list-length (rest list)))))
Use different tools, esp:
- syntax-checker
- stepper
How come we could use list
as an argument — use the syntax checker
(if (null? list)
len
(list-length-helper (rest list) (+ len 1))))
(define (list-length list)
(list-length-helper list 0))
Main idea: lists are a recursive structure, so functions that operate on lists should be recursive functions that follow the recursive definition of lists.
Another example for list function — summing a list of numbers
(if (null? l)
0
(+ (first l) (sum-list (rest l)))))
Also show how to implement rcons
, using this guideline.
More examples:
Define reverse
— solve the problem using rcons
.
rcons
can be generalized into something very useful: append
.
-
How would we use
append
instead ofrcons
? -
How much time will this take? Does it matter if we use
append
orrcons
?
Redefine reverse
using tail recursion.
- Is the result more complex? (Yes, but not too bad because it collects the elements in reverse.)
Tuesday, January 7th Some Style
When you have some common value that you need to use in several places, it is bad to duplicate it. For example:
(cond [(> (* b b) (* 4 a c)) 2]
[(= (* b b) (* 4 a c)) 1]
[(< (* b b) (* 4 a c)) 0]))
What’s bad about it?
-
It’s longer than necessary, which will eventually make your code less readable.
-
It’s slower — by the time you reach the last case, you have evaluated the two sequences three times.
-
It’s more prone to bugs — the above code is short enough, but what if it was longer so you don’t see the three occurrences on the same page? Will you remember to fix all places when you debug the code months after it was written?
In general, the ability to use names is probably the most fundamental concept in computer science — the fact that makes computer programs what they are.
We already have a facility to name values: function arguments. We could split the above function into two like this:
(cond [(> b^2 4ac) 2]
[(= b^2 4ac) 1]
[else 0]))
(define (how-many a b c)
(how-many-helper (* b b) (* 4 a c)))
But instead of the awkward solution of coming up with a new function
just for its names, we have a facility to bind local names — let
. In
general, the syntax for a let
special form is
For example,
But note that the bindings are done “in parallel”, for example, try this:
(let ([x y] [y x])
(list x y)))
(Note that “in parallel” is quoted here because it’s not really parallelism, but just a matter of scopes: the RHSs are all evaluated in the surrounding scope!)
Using this for the above problem:
(let ([b^2 (* b b)]
[4ac (* 4 a c)])
(cond [(> b^2 4ac) 2]
[(= b^2 4ac) 1]
[else 0])))
-
Some notes on writing code (also see the style-guide in the handouts section)
-
Code quality will be graded to in this course!
-
Use abstractions whenever possible, as said above. This is bad:
(define (how-many a b c)
(cond
[(> (* b b) (* 4 a c)) 2]
[(= (* b b) (* 4 a c)) 1]
[(< (* b b) (* 4 a c)) 0]))
(define (what-kind a b c)
(cond
[(= a 0) 'degenerate]
[(> (* b b) (* 4 a c)) 'two]
[(= (* b b) (* 4 a c)) 'one]
[(< (* b b) (* 4 a c)) 'none])) -
But don’t over abstract:
(define one 1)
or(define two "two")
-
Always do test cases, you might want to comment them, but you should always make sure your code works. Use DrRacket’s covergae features to ensure complete coverage.
-
Do not under-document, but also don’t over-document.
-
INDENTATION! (Let DrRacket decide; get used to its rules) –> This is part of the culture that was mentioned last time, but it’s done this way for good reason: decades of programming experience have shown this to be the most readable format. It’s also extremely important to keep good indentation since programmers in all Lisps don’t count parens — they look at the structure.
-
As a general rule,
if
should be either all on one line, or the condition on the first and each consequent on a separate line. Similarly fordefine
— either all on one line or a newline after the object that is being define (either an identifier or a an identifier with arguments). -
Another general rule: you should never have white space after an open-paren, or before a close paren (white space includes newlines). Also, before an open paren there should be either another open paren or white space, and the same goes for after a closing paren.
-
Use the tools that are available to you: for example, use
cond
instead of nestedif
s (definitely do not force the indentation to make a nestedif
look like its C counterpart — remember to let DrRacket indent for you).Another example — do not use
(+ 1 (+ 2 3))
instead of(+ 1 2 3)
(this might be needed in extremely rare situations, only when you know your calculus and have extensive knowledge about round-off errors).Another example — do not use
(cons 1 (cons 2 (cons 3 null)))
instead of(list 1 2 3)
.Also — don’t write things like:
(if (< x 100) #t #f)since it’s the same as just
(< x 100)A few more of these:
(if x #t y) --same-as--> (or x y) ; (almost)
(if x y #f) --same-as--> (and x y) ; (exacly same)
(if x #f #t) --same-as--> (not x) ; (almost)(Actually the first two are almost the same, for example,
(and 1 2)
will return2
, not#t
.) -
Use these as examples for many of these issues:
(define (interest x)
(* x (cond
[(and (> x 0) (<= x 1000)) 0.04]
[(and (> x 1000) (<= x 5000)) 0.045]
[else 0.05])))
(define (how-many a b c)
(cond ((> (* b b) (* (* 4 a) c))
2)
((< (* b b) (* (* 4 a) c))
0)
(else
1)))
(define (what-kind a b c)
(if (equal? a 0) 'degenerate
(if (equal? (how-many a b c) 0) 'zero
(if (equal? (how-many a b c) 1) 'one
'two)
)
)
)
(define (interest deposit)
(cond
[(< deposit 0) "invalid deposit"]
[(and (>= deposit 0) (<= deposit 1000)) (* deposit 1.04) ]
[(and (> deposit 1000) (<= deposit 5000)) (* deposit 1.045)]
[(> deposit 5000) (* deposit 1.05)]))
(define (interest deposit)
(if (< deposit 1001) (* 0.04 deposit)
(if (< deposit 5001) (* 0.045 deposit)
(* 0.05 deposit))))
(define (what-kind a b c) (cond ((= 0 a) 'degenerate)
(else (cond ((> (* b b)(*(* 4 a) c)) 'two)
(else (cond ((= (* b b)(*(* 4 a) c)) 'one)
(else 'none)))))));
Tuesday, January 7th Tail calls
You should generally know what tail calls are, but here’s a quick review
of the subject. A function call is said to be in tail position if there
is no context to “remember” when you’re calling it. Very roughly, this
means that function calls that are not nested as argument expressions of
another call are tail calls. Pay attention that we’re talking about
function calls, not, for example, being nested in an if
expression
since that’s not a function. (The same holds for cond
, and
, or
.)
This definition is something that depends on the context, for example, in an expression like
(foo (add1 (* x 3)))
(foo (/ x 2)))
both calls to foo
are tail calls, but they’re tail calls of this
expression and therefore apply to this context. It might be that this
code is inside another call, as in
(foo (add1 (* x 3)))
(foo (/ x 2)))
something-else)
and the foo
calls are now not in tail position. The main feature of
all Scheme implementations including Racket (and including Javascript)
WRT tail calls is that calls that are in tail position of a function are
said to be “eliminated”. That means that if we’re in an f
function,
and we’re about to call g
in tail position and therefore whatever g
returns would be the result of f
too, then when Racket does the call
to g
it doesn’t bother keeping the f
context — it won’t remember
that it needs to “return” to f
and will instead return straight to its
caller. In other words, when you think about a conventional
implementation of function calls as frames on a stack, Racket will get
rid of a stack frame when it can.
You can also try this with any code in DrRacket: hovering over the paren that starts a function call will show a faint pinkish arrow showing the tail-call chain from there for call that are actually tail calls. This is a simple feature since tail calls are easily identifiable by just looking at the syntax of a function.
Another way to see this is to use DrRacket’s stepper to step through a function call. The stepper is generally an alternative debugger, where instead of visualizing stack frames it assembles an expression that represents these frames. Now, in the case of tail calls, there is no room in such a representation to keep the call — and the thing is that in Racket that’s perfectly fine since these calls are not kept on the call stack.
Note that there are several names for this feature:
-
“Tail recursion”. This is a common way to refer to the more limited optimization of only tail-recursive functions into loops. In languages that have tail calls as a feature, this is too limited, since they also optimize cases of mutual recursion, or any case of a tail call.
-
“Tail call optimization”. In some languages, or more specifically in some compilers, you’ll hear this term. This is fine when tail calls are considered only an “optimization” — but in Racket’s case (as well as Scheme), it’s more than just an optimization: it’s a language feature that you can rely on. For example, a tail-recursive function like
(define (loop) (loop))
must run as an infinite loop, not just optimized to one when the compiler feels like it. -
“Tail call elimination”. This is the so far the most common proper name for the feature: it’s not just recursion, and it’s not an optimization.
When should you use tail calls?
Often, people who are aware of tail calls will try to use them always. That’s not always a good idea. You should generally be aware of the tradeoffs when you consider what style to use. The main thing to remember is that tail-call elimination is a property that helps reducing space use (stack space) — often reducing it from linear space to constant space. This can obviously make things faster, but usually the speedup is just a constant factor since you need to do the same number of iterations anyway, so you just reduce the time spent on space allocation.
Here is one such example that we’ve seen:
(if (null? list)
0
(+ 1 (list-length-1 (rest list)))))
;; versus
(define (list-length-helper list len)
(if (null? list)
len
(list-length-helper (rest list) (+ len 1))))
(define (list-length-2 list)
(list-length-helper list 0))
In this case the first (recursive) version version consumes space linear
to the length of the list, whereas the second version needs only
constant space. But if you consider only the asymptotic runtime, they
are both O(length(l
)).
A second example is a simple implementation of map
:
(if (null? l) l (cons (f (first l)) (map-1 f (rest l)))))
;; versus
(define (map-helper f l acc)
(if (null? l)
(reverse acc)
(map-helper f (rest l) (cons (f (first l)) acc))))
(define (map-2 f l)
(map-helper f l '()))
In this case, both the asymptotic space and the runtime consumption are the same. In the recursive case we have a constant factor for the stack space, and in the iterative one (the tail-call version) we also have a similar factor for accumulating the reversed list. In this case, it is probably better to keep the first version since the code is simpler. In fact, Racket’s stack space management can make the first version run faster than the second — so optimizing it into the second version is useless.
Tuesday, January 7th Sidenote on Types
Note: this is all just a side note for a particularly hairy example. You don’t need to follow all of this to write code in this class! Consider this section a kind of an extra type-related puzzle to read trough, and maybe get back to it much later, after we cover typechecking.
Types can become interestingly complicated when dealing with higher-order functions. Specifically, the nature of the type system used by Typed Racket makes it have one important weakness: it often fails to infer types when there are higher-order functions that operate on polymorphic functions.
For example, consider how map
receives a function and a list of some
type, and applies the function over this list to accumulate its output,
so it’s a polymorphic function with the following type:
But Racket’s map
is actually more flexible that that: it can take more
than a single list input, in which case it will apply the function on
the first element in all lists, then the second and so on. Narrowing our
vision to the two-input-lists case, the type of map
then becomes:
Now, here’s a hairy example — what is the type of this function:
(map map x y))
Begin by what we know — both map
s, call them map1
and map2
, have
the double- and single-list types of map
respectively, here they are,
with different names for types:
map1 : (A B -> C) (Listof A) (Listof B) -> (Listof C)
;; the second `map', consumes a function and one list
map2 : (X -> Y) (Listof X) -> (Listof Y)
Now, we know that map2
is the first argument to map1
, so the type of
map1
s first argument should be the type of map2
:
From here we can conclude that
B = (Listof X)
C = (Listof Y)
If we use these equations in map1
’s type, we get:
(Listof (X -> Y))
(Listof (Listof X))
-> (Listof (Listof Y))
Now, foo
’s two arguments are the 2nd and 3rd arguments of map1
, and
its result is map1
s result, so we can now write our “estimated” type
of foo
:
(Listof (Listof X))
-> (Listof (Listof Y)))
(define (foo x y)
(map map x y))
This should help you understand why, for example, this will cause a type error:
and why this is valid:
But…!
There’s a big “but” here which is that weakness of Typed Racket that was
mentioned. If you try to actually write such a defninition in #lang pl
(which is based on Typed Racket), you will first find that you need to
explicitly list the type variable that are needed to make it into a
generic type. So the above becomes:
(Listof (X -> Y))
(Listof (Listof X))
-> (Listof (Listof Y))))
(define (foo x y)
(map map x y))
But not only does that not work — it throws an obscure type error.
That error is actually due to TR’s weakness: it’s a result of not being
able to infer the proper types. In such cases, TR has two mechanisms to
“guide it” in the right direction. The first one is inst
, which is
used to instantiate a generic (= polymorphic) type some actual type. The
problem here is with the second map
since that’s the polymorphic
function that is given to a higher-order function (the first map
). If
we provide the types to instantiate this, it will work fine:
(Listof (X -> Y))
(Listof (Listof X))
-> (Listof (Listof Y))))
(define (foo x y)
(map (inst map Y X) x y))
Now, you can use this definition to run the above example:
This example works fine, but that’s because we wrote the list argument explicitly. If you try to use the exact example above,
you’d run into the same problem again, since this also uses a
polymorphic function (list
) with a higher-order one (map
). Indeed,
an inst
can make this work for this too:
The second facility is ann
, which can be used to annotate an
expression with the type that you expect it to have.
(map (ann map ((X -> Y) (Listof X) -> (Listof Y)))
x y))
(Note: this is not type casting! It’s using a different type which is
also applicable for the given expression, and having the type checker
validate that this is true. TR does have a similar cast
form, which is
used for a related but different cases.)
This tends to be more verbose than inst
, but is sometimes easier to
follow, since the expected type is given explicitly. The thing about
inst
is that it’s kind of “applying” a polymorphic (All (A B) ...)
type, so you need to know the order of the A B
arguments, which is why
in the above we use (inst map Y X)
rather than (inst map X Y)
.
Again, remember that this is all not something that you need to know. We will have a few (very rare) cases where we’ll need to use
inst
, and in each of these, you’ll be told where and how to use it.
Tuesday, January 7th Side-note: Names are important
An important “discovery” in computer science is that we don’t need names for every intermediate sub-expression — for example, in almost any language we can write something like:
instead of
y₁ = 4 * a
y₂ = y * c
x₂ = x - y
x₃ = sqrt(x)
y₃ = -b
x₄ = y + x
y₄ = 2 * a
s = x / y
Such languages are put in contrast to assembly languages, and were all put under the generic label of “high level languages”.
(Here’s an interesting idea — why not do the same for function values?)
Tuesday, January 14th BNF, Grammars, the AE Language
Getting back to the theme of the course: we want to investigate programming languages, and we want to do that using a programming language.
The first thing when we design a language is to specify the language. For this we use BNF (Backus-Naur Form). For example, here is the definition of a simple arithmetic language:
| <AE> + <AE>
| <AE> - <AE>
Explain the different parts. Specifically, this is a mixture of low-level (concrete) syntax definition with parsing.
We use this to derive expressions in some language. We start with
<AE>
, which should be one of these:
- a number
<num>
- an
<AE>
, the text “+
”, and another<AE>
- the same but with “
-
”
<num>
is a terminal: when we reach it in the derivation, we’re done.
<AE>
is a non-terminal: when we reach it, we have to continue with one
of the options. It should be clear that the +
and the -
are things
we expect to find in the input — because they are not wrapped in
<>
s.
We could specify what <num>
is (turning it into a <NUM>
non-terminal):
| <AE> + <AE>
| <AE> - <AE>
<NUM> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
| <NUM> <NUM>
But we don’t — why? Because in Racket we have numbers as primitives and we want to use Racket to implement our languages. This makes life a lot easier, and we get free stuff like floats, rationals etc.
To use a BNF formally, for example, to prove that 1-2+3
is a valid
<AE>
expression, we first label the rules:
| <AE> + <AE> (2)
| <AE> - <AE> (3)
and then we can use them as formal justifications for each derivation step:
<AE> + <AE> ; (2)
<AE> + <num> ; (1)
<AE> - <AE> + <num> ; (3)
<AE> - <AE> + 3 ; (num)
<num> - <AE> + 3 ; (1)
<num> - <num> + 3 ; (1)
1 - <num> + 3 ; (num)
1 - 2 + 3 ; (num)
This would be one way of doing this. Alternatively, we can can visualize the derivation using a tree, with the rules used at the nodes.
These specifications suffer from being ambiguous: an expression can be
derived in multiple ways. Even the little syntax for a number is
ambiguous — a number like 123
can be derived in two ways that result
in trees that look different. This ambiguity is not a “real” problem
now, but it will become one very soon. We want to get rid of this
ambiguity, so that there is a single (= deterministic) way to derive all
expressions.
There is a standard way to resolve that — we add another non-terminal to the definition, and make it so that each rule can continue to exactly one of its alternatives. For example, this is what we can do with numbers:
<DIGIT> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Similar solutions can be applied to the <AE>
BNF — we either
restrict the way derivations can happen or we come up with new
non-terminals to force a deterministic derivation trees.
As an example of restricting derivations, we look at the current grammar:
| <AE> + <AE>
| <AE> - <AE>
and instead of allowing an <AE>
on both sides of the operation, we
force one to be a number:
| <num> + <AE>
| <num> - <AE>
Now there is a single way to derive any expression, and it is always
associating operations to the right: an expression like 1+2+3
can only
be derived as 1+(2+3)
. To change this to left-association, we would
use this:
| <AE> + <num>
| <AE> - <num>
But what if we want to force precedence? Say that our AE syntax has addition and multiplication:
| <AE> + <AE>
| <AE> * <AE>
We can do that same thing as above and add new non-terminals — say one for “products”:
| <AE> + <AE>
| <PROD>
<PROD> ::= <num>
| <PROD> * <PROD>
Now we must parse any AE expression as additions of multiplications (or
numbers). First, note that if <AE>
goes to <PROD>
and that goes to
<num>
, then there is no need for an <AE>
to go to a <num>
, so this
is the same syntax:
| <PROD>
<PROD> ::= <num>
| <PROD> * <PROD>
Now, if we want to still be able to multiply additions, we can force them to appear in parentheses:
| <PROD>
<PROD> ::= <num>
| <PROD> * <PROD>
| ( <AE> )
Next, note that <AE>
is still ambiguous about additions, which can be
fixed by forcing the left hand side of an addition to be a factor:
| <PROD>
<PROD> ::= <num>
| <PROD> * <PROD>
| ( <AE> )
We still have an ambiguity for multiplications, so we do the same thing and add another non-terminal for “atoms”:
| <PROD>
<PROD> ::= <ATOM> * <PROD>
| <ATOM>
<ATOM> ::= <num>
| ( <AE> )
And you can try to derive several expressions to be convinced that derivation is always deterministic now.
But as you can see, this is exactly the cosmetics that we want to avoid — it will lead us to things that might be interesting, but unrelated to the principles behind programming languages. It will also become much much worse when we have a real language rather such a tiny one.
Is there a good solution? — It is right in our face: do what Racket does — always use fully parenthesized expressions:
| ( <AE> + <AE> )
| ( <AE> - <AE> )
To prevent confusing Racket code with code in our language(s), we also change the parentheses to curly ones:
| { <AE> + <AE> }
| { <AE> - <AE> }
But in Racket everything has a value — including those +
s and
-
s, which makes this extremely convenient with future operations that
might have either more or less arguments than 2 as well as treating
these arithmetic operators as plain functions. In our toy language we
will not do this initially (that is, +
and -
are second order
operators: they cannot be used as values). But since we will get to it
later, we’ll adopt the Racket solution and use a fully-parenthesized
prefix notation:
| { + <AE> <AE> }
| { - <AE> <AE> }
(Remember that in a sense, Racket code is written in a form of already-parsed syntax…)
Tuesday, January 14th Simple Parsing
On to an implementation of a “parser”:
Unrelated to what the syntax actually looks like, we want to parse it as soon as possible — converting the concrete syntax to an abstract syntax tree.
No matter how we write our syntax:
3+4
(infix),3 4 +
(postfix),+(3,4)
(prefix with args in parens),(+ 3 4)
(parenthesized prefix),
we always mean the same abstract thing — adding the number 3
and the
number 4
. The essence of this is basically a tree structure with an
addition operation as the root and two leaves holding the two numerals.
With the right data definition, we can describe this in Racket as the
expression (Add (Num 3) (Num 4))
where Add
and Num
are
constructors of a tree type for syntax, or in a C-like language, it
could be something like Add(Num(3),Num(4))
.
Similarly, the expression (3-4)+7
will be described in Racket as the
expression:
Important note: “expression” was used in two different ways in the above — each way corresponds to a different language, and the result of evaluating the second “expression” is a Racket value that represents the first expression.
To define the data type and the necessary constructors we will use this:
[Num Number]
[Add AE AE]
[Sub AE AE])
-
Note — Racket follows the tradition of Lisp which makes syntax issues almost negligible — the language we use is almost as if we are using the parse tree directly. Actually, it is a very simple syntax for parse trees, one that makes parsing extremely easy.
[This has an interesting historical reason… Some Lisp history — M-expressions vs. S-expressions, and the fact that we write code that is isomorphic to an AST. Later we will see some of the advantages that we get by doing this. See also “The Evolution of Lisp”, section 3.5.1. Especially the last sentence:
Therefore we expect future generations of Lisp programmers to continue to reinvent Algol-style syntax for Lisp, over and over and over again, and we are equally confident that they will continue, after an initial period of infatuation, to reject it. (Perhaps this process should be regarded as a rite of passage for Lisp hackers.)
And an interesting & modern counter-example of this here.]
To make things very simple, we will use the above fact through a double-level approach:
-
we first “parse” our language into an intermediate representation — a Racket list — this is mostly done by a modified version of Racket’s
read
function that uses curly{}
braces instead of round()
parens, -
then we write our own
parse
function that will parse the resulting list into an instance of theAE
type — an abstract syntax tree (AST).
This is achieved by the following simple recursive function:
;; parses s-expressions into AEs
(define (parse-sexpr sexpr)
(cond [(number? sexpr) (Num sexpr)]
[(and (list? sexpr) (= 3 (length sexpr)))
(let ([make-node
(match (first sexpr)
['+ Add]
['- Sub]
[else (error 'parse-sexpr "unknown op: ~s"
(first sexpr))])
#| the above is the same as:
(cond [(equal? '+ (first sexpr)) Add]
[(equal? '- (first sexpr)) Sub]
[else (error 'parse-sexpr "unknown op: ~s"
(first sexpr))])
|#])
(make-node (parse-sexpr (second sexpr))
(parse-sexpr (third sexpr))))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
This function is pretty simple, but as our languages grow, they will
become more verbose and more difficult to write. So, instead, we use a
new special form: match
, which is matching a value and binds new
identifiers to different parts (try it with “Check Syntax”). Re-writing
the above code using match
:
;; parses s-expressions into AEs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(list '+ left right)
(Add (parse-sexpr left) (parse-sexpr right))]
[(list '- left right)
(Sub (parse-sexpr left) (parse-sexpr right))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
And finally, to make it more uniform, we will combine this with the function that parses a string into a sexpr so we can use strings to represent our programs:
;; parses a string containing an AE expression to an AE
(define (parse str)
(parse-sexpr (string->sexpr str)))
Tuesday, January 14th The match
Form
The syntax for match
is
[pattern result-expr]
...)
The value is matched against each pattern, possibly binding names in the process, and if a pattern matches it evaluates the result expression. The simplest form of a pattern is simply an identifier — it always matches and binds that identifier to the value:
[x x]) ; evaluates to the list
Another simple pattern is a quoted symbol, which matches that symbol. For example:
['x "yes"]
[else "no"])
will evaluate to "yes"
if foo
is the symbol x
, and to "no"
otherwise. Note that else
is not a keyword here — it happens to be a
pattern that always succeeds, so it behaves like an else clause except
that it binds else
to the unmatched-so-far value.
Many patterns look like function application — but don’t confuse them
with applications. A (list x y z)
pattern matches a list of exactly
three items and binds the three identifiers; or if the “arguments” are
themselves patterns, match
will descend into the values and match them
too. More specifically, this means that patterns can be nested:
[(list x y z) (+ x y z)]) ; evaluates to 6
(match (list 1 2 3)
[(cons x (list y z)) (+ x y z)]) ; matches the same shape (also 6)
(match '((1) (2) 3)
[(list (list x) (list y) z) (+ x y z)]) ; also 6
As seen above, there is also a cons
pattern that matches a non-empty
list and then matches the first part against the head for the list and
the second part against the tail of the list.
In a list
pattern, you can use ...
to specify that the previous
pattern is repeated zero or more times, and bound names get bound to the
list of respective matching. One simple consequent is that the (list hd tl ...)
pattern is exactly the same as (cons hd tl)
, but being able
to repeat an arbitrary pattern is very useful:
[(list (list x y) ...) (list x y)])
'((1 3 5 7) (2 4 6 8))
A few more useful patterns:
_ -- matches anything, but does not bind
(number: n) -- matches any number and binds it to `n'
(symbol: s) -- same for symbols
(string: s) -- strings
(sexpr: s) -- S-expressions (needed sometimes for Typed Racket)
(and pat1 pat2) -- matches both patterns
(or pat1 pat2) -- matches either pattern (careful with bindings)
Note that the foo:
patterns are all specific to our #lang pl
, they
are not part of #lang racket
or #lang typed/racket
.
The patterns are tried one by one in-order, and if no pattern matches the value, an error is raised.
Note that ...
in a list
pattern can follow any pattern, including
all of the above, and including nested list patterns.
Here are a few examples — you can try them out with #lang pl untyped
at the top of the definitions window. This:
[(list (symbol: syms) ...) syms])
matches x
against a pattern that accepts only a list of symbols, and
binds syms
to those symbols. If you want to match only a list of, say,
one or more symbols, then just add one before the ...
-ed pattern
variable:
[(list (symbol: sym) (symbol: syms) ...) syms])
;; same as:
(match x
[(cons (symbol: sym) (list (symbol: syms) ...)) syms])
which will match such a non-empty list, where the whole list (on the
right hand side) is (cons sym syms)
.
Here’s another example that matches a list of any number of lists, where
each of the sub-lists begins with a symbol and then has any number of
numbers. Note how the n
and s
bindings get values for a list of all
symbols and a list of lists of the numbers:
(match x
[(list (list (symbol: s) (number: n) ...) ...)
(list 'symbols: s 'numbers: n)]))
> (foo (list (list 'x 1 2 3) (list 'y 4 5)))
'(symbols: (x y) numbers: ((1 2 3) (4 5)))
Here is a quick example for how or
is used with two literal
alternatives, how and
is used to name a specific piece of data, and
how or
is used with a binding:
(match x
[(list (or 1 2 3)) 'single]
[(list (and x (list 1 _)) 2) x]
[(or (list 1 x) (list 2 x)) x]))
> (foo (list 3))
'single
> (foo (list (list 1 99) 2))
'(1 99)
> (foo (list 1 10))
10
> (foo (list 2 10))
10
Tuesday, January 14th The define-type
Form
The class language that we’re using, #lang pl
, is based on Typed
Racket: a statically-typed dialect of Racket. It is not exactly the
same as Typed Racket — it is restricted in many ways, and extended in
a few ways. (You should therefore try to avoid looking at the Typed
Racket documentation and expect things to be the same in #lang pl
.)
The most important extension is define-type
, which is the construct we
will be using to create new user-defined types. In general, such
definitions looks like what we just used:
[Num Number]
[Add AE AE]
[Sub AE AE])
This defines a new type called AE
, an AE?
predicate for this type,
and a few variants for this type: Num
, Add
, and Sub
in this
case. Each of these variant names is a constructor, taking in arguments
with the listed types, where these types can include the newly defined
type itself in (the very common) case we’re defining a recursive type.
The return type is always the newly defined type, AE
here.
To summarize, this definition gives us a new AE
type, and three
constructors, as if we wrote the following type declarations:
(: Num : Number -> AE)
(: Add : AE AE -> AE)
(: Sub : AE AE -> AE)
The newly defined types are known as “disjoint unions”, since values in these types are disjoint — there is no overlap between the different variants. As we will see, this is what makes this such a useful construct for our needs: the compiler knows about the variants of each newly defined type, which will make it possible for it to complain if we extend a type with more variants but not update all uses of the type.
Furthermore, since the return types of these constructors are all the
new type itself, there is no way for us to write code that expects
just one of these variants. We will use a second form, cases
, to
handle these values.
Tuesday, January 14th The cases
Form
A define-type
declaration defines only what was described above: one
new type name and a matching predicate, and a few variants as
constructor functions. Unlike HtDP, we don’t get predicates for each of
the variants, and we don’t get accessor functions for the fields of the
variants.
The way that we handle the new kind of values is with cases
: this is a
form that is very similar to match
, but is specific to instances of
the user-defined type.
Many students find it confusing to distinguish
match
andcases
since they are so similar. Try to remember thatmatch
is for primitive Racket values (we’ll mainly use them for S-expression values), whilecases
is for user-defined values. The distinction between the two forms is unfortunate, and doesn’t serve any purpose. It is just technically difficult to unify the two.
For example, code that handles AE
values (as defined above) can look
as follows:
[(Num n) "a number"]
[(Add l r) "an addition"]
[(Sub l r) "a subtraction"])
As you can see, we need to have patterns for each of the listed variants (and the compiler will throw an error if some are missing), and each of these patterns specifies bindings that will get the field values contained in a given variant object.
We can also use nested patterns:
[(Num n) "a number"]
[(Add (Num m) (Num n)) "a simple addition"]
[(Add l r) "an addition"]
[(Sub (Num m) (Num n)) "a simple subtraction"]
[(Sub l r) "a subtraction"])
but this is a feature that we will not use too often.
The final clause in a cases
form can be an else
clause, which serves
as a fallback in case none of the previous clauses matched the input
value. However, using an else
like this is strongly discouraged!
The problem with using it is that it effectively eliminates the
advantage in getting the type-checker to complain when a type definition
is extended with new variants. Using these else
clauses, we can
actually mimic all of the functionality that you expect in HtDP-style
code, which demonstrates that this is equivalent to HtDP-style
definitions. For example:
;; identifies instances of the `Add` variant
(define (Add? ae)
(cases ae
[(Add l r) #t]
[else #f]))
(: Add-left : AE -> AE)
;; get the left-hand subexpression of an addition
(define (Add-left ae)
(cases ae
[(Add l r) l]
[else (error 'Add-left "expecting an Add value, got ~s" ae)]))
...
Important reminder: this is code that you should not write!
Doing so will lead to code that is more fragile than just using cases
,
since you’d be losing the protection the compiler gives you in the form
of type errors on occurrences of cases
that need to be updated when a
type is extended with new variants. You would therefore end up writing a
bunch of boiler-plate code only to end up with lower-quality code. The
core of the problem is in the prevalent use of else
which gives up
that protection.
In these examples the else
clause is justified because even if AE
is
extended with new variants, functions like Add?
and Add-left
should
not be affected and treat the new variants as they treat all other
non-Add
instances. (And since else
is inherent to these functions,
using them in our code is inherently a bad idea.) We will, however, have
a few (very few!) places where we’ll need to use else
— but this
will always be done only on some specific functionality rather than a
wholesale approach of defining a different interface for user-defined
types.
Tuesday, January 14th Semantics (= Evaluation)
Back to BNF — now, meaning.
An important feature of these BNF specifications: we can use the derivations to specify meaning (and meaning in our context is “running” a program (or “interpreting”, “compiling”, but we will use “evaluating”)). For example:
| <AE1> + <AE2> ; <AE> evaluates to the sum of evaluating
; <AE1> and <AE2>
| <AE1> - <AE2> ; ... the subtraction of <AE2> from <AE1>
(... roughly!)
To do this a little more formally:
b. eval(<AE1> + <AE2>) = eval(<AE1>) + eval(<AE2>)
c. eval(<AE1> - <AE2>) = eval(<AE1>) - eval(<AE2>)
Note the completely different roles of the two +
s and -
s. In fact,
it might have been more correct to write:
b. eval("<AE1> + <AE2>") = eval("<AE1>") + eval("<AE2>")
c. eval("<AE1> - <AE2>") = eval("<AE1>") - eval("<AE2>")
or even using a marker to denote meta-holes in these strings:
b. eval("$<AE1> + $<AE2>") = eval("$<AE1>") + eval("$<AE2>")
c. eval("$<AE1> - $<AE2>") = eval("$<AE1>") - eval("$<AE2>")
but we will avoid pretending that we’re doing that kind of string
manipulation. (For example, it will require specifying what does it mean
to return <num>
for $<num>
(involves string->number
), and the
fragments on the right side mean that we need to specify these as
substring operations.)
Note that there’s a similar kind of informality in our BNF
specifications, where we assume that <foo>
refers to some terminal or
non-terminal. In texts that require more formal specifications (for
example, in RFC specifications), each literal part of the BNF is usually
double-quoted, so we’d get
An alternative popular notation for eval(X)
is ⟦X⟧
:
b. [[<AE1> + <AE2>]] = [[<AE1>]] + [[<AE2>]]
c. [[<AE1> - <AE2>]] = [[<AE1>]] - [[<AE2>]]
Is there a problem with this definition? Ambiguity:
Depending on the way the expression is parsed, we can get either a
result of 2
or -4
:
= eval(1) - eval(2) + eval(3) [c]
= 1 - 2 + 3 [a,a,a]
= 2
eval(1 - 2 + 3) = eval(1) - eval(2 + 3) [c]
= eval(1) - (eval(2) + eval(3)) [a]
= 1 - (2 + 3) [a,a,a]
= -4
Again, be very aware of confusing subtleties which are extremely important: We need parens around a sub-expression only in one side, why? — When we write:
we have two expressions, but one stands for an input syntax, and one stands for a real mathematical expression.
In a case of a computer implementation, the syntax on the left is (as always) an AE syntax, and the real expression on the right is an expression in whatever language we use to implement our AE language.
Like we said earlier, ambiguity is not a real problem until the actual
parse tree matters. With eval
it definitely matters, so we must not
make it possible to derive any syntax in multiple ways or our evaluation
will be non-deterministic.
Quick exercise:
We can define a meaning for <digit>
s and then <num>
s in a similar
way:
eval(0) = 0
eval(1) = 1
eval(2) = 2
...
eval(9) = 9
eval(<digit>) = <digit>
eval(<digit> <NUM>) = 10*eval(<digit>) + eval(<NUM>)
Is this exactly what we want? — Depends on what we actually want…
-
First, there’s a bug in this code — having a BNF derivation like
<NUM> ::= <digit> | <digit> <NUM>is unambiguous, but makes it hard to parse a number. We get:
eval(123) = 10*eval(1) + eval(23)
= 10*1 + 10*eval(2) + eval(3)
= 10*1 + 10*2 + 3
= 33Changing the order of the last rule works much better:
<NUM> ::= <digit> | <NUM> <digit>and then:
eval(<NUM> <digit>) = 10*eval(<NUM>) + eval(<digit>) -
As a concrete example see how you would make it work with
107
, which demonstrates why compositionality is important. -
Example for free stuff that looks trivial: if we were to define the meaning of numbers this way, would it always work? Think an average language that does not give you bignums, making the above rules fail when the numbers are too big. In Racket, we happen to be using an integer representation for the syntax of integers, and both are unlimited. But what if we wanted to write a Racket compiler in C or a C compiler in Racket? What about a C compiler in C, where the compiler runs on a 64 bit machine, and the result needs to run on a 32 bit machine?
Tuesday, January 14th Side-note: Compositionality
The example of
being a language that is easier to write an evaluator for leads us to an
important concept — compositionality. This definition is easier to
write an evaluator for, since the resulting language is compositional:
the meaning of an expression — for example 123
— is composed out
of the meaning of its two parts, which in this BNF are 12
and 3
.
Specifically, the evaluation of <NUM> <digit>
is 10 *
the evaluation
of the first, plus the evaluation of the second. In the <digit> <NUM>
case this is more difficult — the meaning of such a number depends not
only on the meaning of the two parts, but also on the <NUM>
syntax:
eval(<digit>) * 10^length(<NUM>) + eval(<NUM>)
This this case this can be tolerable, since the meaning of the
expression is still made out of its parts — but imperative programming
(when you use side effects) is much more problematic since it is not
compositional (at least not in the obvious sense). This is compared to
functional programming, where the meaning of an expression is a
combination of the meanings of its subexpressions. For example, every
sub-expression in a functional program has some known meaning, and these
all make up the meaning of the expression that contains them — but in
an imperative program we can have a part of the code be x++
— and
that doesn’t have a meaning by itself, at least not one that contributes
to the meaning of the whole program in a direct way.
(Actually, we can have a well-defined meaning for such an expression:
the meaning is going from a world where x
is a container of some value
N, to a world where the same container has a different value N+1. You
can probably see now how this can make things more complicated. On an
intuitive level — if we look at a random part of a functional program
we can tell its meaning, so building up the meaning of the whole code is
easy, but in an imperative program, the meaning of a random part is
pretty much useless.)
Tuesday, January 14th Implementing an Evaluator
Now continue to implement the semantics of our syntax — we express
that through an eval
function that evaluates an expression.
We use a basic programming principle — splitting the code into two layers, one for parsing the input, and one for doing the evaluation. Doing this avoids the mess we’d get into otherwise, for example:
(match sexpr
[(number: n) n]
[(list '+ left right) (+ (eval left) (eval right))]
[(list '- left right) (- (eval left) (eval right))]
[else (error 'eval "bad syntax in ~s" sexpr)]))
This is messy because it combines two very different things — syntax and semantics — into a single lump of code. For this particular kind of evaluator it looks simple enough, but this is only because it’s simple enough that all we do is replace constructors by arithmetic operations. Later on things will get more complex, and bundling the evaluator with the parser will be more problematic. (Note: the fact that we can replace constructors with the run-time operators mean that we have a very simple, calculator-like language, and that we can, in fact, “compile” all programs down to a number.)
If we split the code, we can easily include decisions like making
syntactically invalid. (Which is not, BTW, what Racket does…) (Also, this is like the distinction between XML syntax and well-formed XML syntax.)
An additional advantage is that by using two separate components, it is simple to replace each one, making it possible to change the input syntax, and the semantics independently — we only need to keep the same interface data (the AST) and things will work fine.
Our parse
function converts an input syntax to an abstract syntax tree
(AST). It is abstract exactly because it is independent of any actual
concrete syntax that you type in, print out etc.
Tuesday, January 14th Implementing The AE Language
Back to our eval
— this will be its (obvious) type:
;; consumes an AE and computes
;; the corresponding number
which leads to some obvious test cases:
(equal? 7 (eval (parse "{+ 3 4}")))
(equal? 6 (eval (parse "{+ {- 3 4} 7}")))
which from now on we will write using the new test
form that the
#lang pl
language provides:
(test (eval (parse "{+ 3 4}")) => 7)
(test (eval (parse "{+ {- 3 4} 7}")) => 6)
Note that we’re testing only at the interface level — only running whole functions. For example, you could think about a test like:
=> (Add (Sub (Num 3) (Num 4)) (Num 7)))
but the details of parsing and of the constructor names are things that
nobody outside of our evaluator cares about — so we’re not testing
them. In fact, we shouldn’t even mention parse
in these tests, since
it is not part of the public interface of our users; they only care
about using it as a compiler-like black box. (This is sometimes called
“integration tests”.) We’ll address this shortly.
Like everything else, the structure of the recursive eval
code follows
the recursive structure of its input. In HtDP terms, our template is:
(define (eval expr)
(cases expr
[(Num n) ... n ...]
[(Add l r) ... (eval l) ... (eval r) ...]
[(Sub l r) ... (eval l) ... (eval r) ...]))
In this case, filling in the gaps is very simple
(define (eval expr)
(cases expr
[(Num n) n]
[(Add l r) (+ (eval l) (eval r))]
[(Sub l r) (- (eval l) (eval r))]))
We now further combine eval
and parse
into a single run
function
that evaluates an AE string.
;; evaluate an AE program contained in a string
(define (run str)
(eval (parse str)))
This function becomes the single public entry point into our code, and the only thing that should be used in tests that verify our interface:
(test (run "{+ 3 4}") => 7)
(test (run "{+ {- 3 4} 7}") => 6)
The resulting full code is:
ae.rkt D #lang pl
#| BNF for the AE language:
<AE> ::= <num>
| { + <AE> <AE> }
| { - <AE> <AE> }
| { * <AE> <AE> }
| { / <AE> <AE> }
|#
;; AE abstract syntax trees
(define-type AE
[Num Number]
[Add AE AE]
[Sub AE AE]
[Mul AE AE]
[Div AE AE])
(: parse-sexpr : Sexpr -> AE)
;; parses s-expressions into AEs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
(: parse : String -> AE)
;; parses a string containing an AE expression to an AE AST
(define (parse str)
(parse-sexpr (string->sexpr str)))
(: eval : AE -> Number)
;; consumes an AE and computes the corresponding number
(define (eval expr)
(cases expr
[(Num n) n]
[(Add l r) (+ (eval l) (eval r))]
[(Sub l r) (- (eval l) (eval r))]
[(Mul l r) (* (eval l) (eval r))]
[(Div l r) (/ (eval l) (eval r))]))
(: run : String -> Number)
;; evaluate an AE program contained in a string
(define (run str)
(eval (parse str)))
;; tests
(test (run "3") => 3)
(test (run "{+ 3 4}") => 7)
(test (run "{+ {- 3 4} 7}") => 6)
(Note that the tests are done with a test
form, which we mentioned
above.)
For anyone who thinks that Racket is a bad choice, this is a good point to think how much code would be needed in some other language to do the same as above.
Tuesday, January 14th Intro to Typed Racket
The plan:
-
Why Types?
-
Why Typed Racket?
-
What’s Different about Typed Racket?
-
Some Examples of Typed Racket for Course Programs
Types
-
Who has used a (statically) typed language?
-
Who has used a typed language that’s not Java?
Typed Racket will be both similar to and very different from anything you’ve seen before.
Why types?
-
Types help structure programs.
-
Types provide enforced and mandatory documentation.
-
Types help catch errors.
Types will help you. A lot.
Structuring programs
-
Data definitions
;; An AE is one of: ; \
;; (make-Num Number) ; > HtDP
;; (make-Add AE AE) ; /
(define-type AE ; \
[Num number?] ; > Predicates =~= contracts (PLAI)
[Add AE? AE?]) ; / (has names of defined types too)
(define-type AE ; \
[Num Number] ; > Typed Racket (our PL)
[Add AE AE]) ; / -
Data-first
The structure of your program is derived from the structure of your data.
You have seen this in Fundamentals with the design recipe and with templates. In this class, we will see it extensively with type definitions and the (cases …) form. Types make this pervasive — we have to think about our data before our code.
-
A language for describing data
Instead of having an informal language for describing types in contract lines, and a more formal description of predicates in a
define-type
form, we will have a single, unified language for both of these. Having such a language means that we get to be more precise and more expressive (since the typed language covers cases that you would otherwise dismiss with some hand waving, like “a function”).
Why Typed Racket?
Racket is the language we all know, and it has the benefits that we discussed earlier. Mainly, it is an excellent language for experimenting with programming languages.
-
Typed Racket allows us to take our Racket programs and typecheck them, so we get the benefits of a statically typed language.
-
Types are an important programming language feature; Typed Racket will help us understand them.
[Also: the development of Typed Racket is happening here in Northeastern, and will benefit from your feedback.]
How is Typed Racket different from Racket
-
Typed Racket will reject your program if there are type errors! This means that it does that at compile-time, before any code gets to run.
-
Typed Racket files start like this:
#lang typed/racket
;; Program goes here.but we will use a variant of the Typed Racket language, which has a few additional constructs:
#lang pl
;; Program goes here. -
Typed Racket requires you to write the contracts on your functions.
Racket:
;; f : Number -> Number
(define (f x)
(* x (+ x 1)))Typed Racket:
#lang pl
(: f : Number -> Number)
(define (f x)
(* x (+ x 1)))[In the “real” Typed Racket the preferred style is with prefix arrows:
#lang typed/racket
(: f (-> Number Number))
(define (f x) : Number
(* x (+ x 1)))and you can also have the type annotations appear inside the definition:
#lang typed/racket
(define (f [x : Number]) : Number
(* x (+ x 1)))but we will not use these form.]
-
As we’ve seen, Typed Racket uses types, not predicates, in
define-type
.(define-type AE
[Num Number]
[Add AE AE])versus
(define-type AE
[Num number?]
[Add AE? AE?]) -
There are other differences, but these will suffice for now.
Examples
(define (digit-num n)
(cond [(<= n 9) 1]
[(<= n 99) 2]
[(<= n 999) 3]
[(<= n 9999) 4]
[else "a lot"]))
(: fact : Number -> Number)
(define (fact n)
(if (zero? n)
1
(* n (fact (- n 1)))))
(: helper : Number Number -> Number)
(define (helper n acc)
(if (zero? n)
acc
(helper (- n 1) (* acc n))))
(: fact : Number -> Number)
(define (fact n)
(helper n 1))
(: fact : Number -> Number)
(define (fact n)
(: helper : Number Number -> Number)
(define (helper n acc)
(if (zero? n)
acc
(helper (- n 1) (* acc n))))
(helper n 1))
(: every? : (All (A) (A -> Boolean) (Listof A) -> Boolean))
;; Returns false if any element of lst fails the given pred,
;; true if all pass pred.
(define (every? pred lst)
(or (null? lst)
(and (pred (first lst))
(every? pred (rest lst)))))
(define-type AE
[Num Number]
[Add AE AE]
[Sub AE AE])
;; the only difference in the following definition is
;; using (: <name> : <type>) instead of ";; <name> : <type>"
(: parse-sexpr : Sexpr -> AE)
;; parses s-expressions into AEs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(list '+ left right)
(Add (parse-sexpr left) (parse-sexpr right))]
[(list '- left right)
(Sub (parse-sexpr left) (parse-sexpr right))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
More interesting examples
-
Typed Racket is designed to be a language that is friendly to the kind of programs that people write in Racket. For example, it has unions:
(: foo : (U String Number) -> Number)
(define (foo x)
(if (string? x)
(string-length x)
;; at this point it knows that `x' is not a
;; string, therefore it must be a number
(+ 1 x)))This is not common in statically typed languages, which are usually limited to only disjoint unions. For example, in OCaml you’d write this definition:
type string_or_number = Str of string | Int of int ;;
let foo x = match x with Str s -> String.length s
| Int i -> i+1 ;;And use it with an explicit constructor:
foo (Str "bar") ;;
foo (Int 3) ;; -
Note that in the Typed Racket case, the language keeps track of information that is gathered via predicates — which is why it knows that one
x
is a String, and the other is a Number. -
Typed Racket has a concept of subtypes — which is also something that most statically typed languages lack. In fact, the fact that it has (arbitrary) unions means that it must have subtypes too, since a type is always a subtype of a union that contains this type.
-
Another result of this feature is that there is an
Any
type that is the union of all other types. Note that you can always use this type since everything is in it — but it gives you the least information about a value. In other words, Typed Racket gives you a choice: you decide which type to use, one that is very restricted but has a lot of information about its values to a type that is very permissive but has almost no useful information. This is in contrast to other type system (HM systems) where there is always exactly one correct type.To demonstrate, consider the identity function:
(define (id x) x)You could use a type of
(: id : Integer -> Integer)
which is very restricted, but you know that the function always returns an integer value.Or you can make it very permissive with a
(: id : Any -> Any)
, but then you know nothing about the result — in fact,(+ 1 (id 2))
will throw a type error. It does return2
, as expected, but the type checker doesn’t know the type of that2
. If you wanted to use this type, you’d need to check that the result is a number, eg:(let ([x (id 123)]) (if (number? x) (+ x 10) 999))This means that for this particular function there is no good specific type that we can choose — but there are polymorphic types. These types allow propagating their input type(s) to their output type. In this case, it’s a simple “my output type is the same as my input type”:
(: id : (All (A) A -> A))This makes the output preserve the same level of information that you had on its input.
-
Another interesting thing to look at is the type of
error
: it’s a function that returns a type ofNothing
— a type that is the same as an empty union:(U)
. It’s a type that has no values in it — it fitserror
because it is a function that doesn’t return any value, in fact, it doesn’t return at all. In addition, it means that anerror
expression can be used anywhere you want because it is a subtype of anything at all. -
An
else
clause in acond
expression is almost always needed, for example:(: digit-num : Number -> (U Number String))
(define (digit-num n)
(cond [(<= n 9) 1]
[(<= n 99) 2]
[(<= n 999) 3]
[(<= n 9999) 4]
[(> n 9999) "a lot"]))(and if you think that the type checker should know what this is doing, then how about
(> (* n 10) (/ (* (- 10000 1) 20) 2))or
(>= n 10000)for the last test?)
-
In some rare cases you will run into one limitation of Typed Racket: it is difficult (that is: a generic solution is not known at the moment) to do the right inference when polymorphic functions are passed around to higher-order functions. For example:
(: call : (All (A B) (A -> B) A -> B))
(define (call f x)
(f x))
(call rest (list 4))In such cases, we can use
inst
to instantiate a function with a polymorphic type to a given type — in this case, we can use it to make it treatrest
as a function that is specific for numeric lists:(call (inst rest Number) (list 4))In other rare cases, Typed Racket will infer a type that is not suitable for us — there is another form,
ann
, that allows us to specify a certain type. Using this in thecall
example is more verbose:(call (ann rest : ((Listof Number) -> (Listof Number))) (list 4))However, these are going to be rare and will be mentioned explicitly whenever they’re needed.
Tuesday, January 14th Bindings & Substitution
We now get to an important concept: substitution.
Even in our simple language, we encounter repeated expressions. For example, if we want to compute the square of some expression:
Why would we want to get rid of the repeated sub-expression?
-
It introduces a redundant computation. In this example, we want to avoid computing the same sub-expression a second time.
-
It makes the computation more complicated than it could be without the repetition. Compare the above with:
with x = {+ 4 2},
{* x x} -
This is related to a basic fact in programming that we have already discussed: duplicating information is always a bad thing. Among other bad consequences, it can even lead to bugs that could not happen if we wouldn’t duplicate code. A toy example is “fixing” one of the numbers in one expression and forgetting to fix the other:
{* {+ 4 2} {+ 4 1}}Real world examples involve much more code, which make such bugs very difficult to find, but they still follow the same principle.
-
This gives us more expressive power — we don’t just say that we want to multiply two expressions that both happen to be
{+ 4 2}
, we say that we multiply the{+ 4 2}
expression by itself. It allows us to express identity of two values as well as using two values that happen to be the same.
So, the normal way to avoid redundancy is to introduce an identifier. Even when we speak, we might say: “let x be 4 plus 2, multiply x by x”.
(These are often called “variables”, but we will try to avoid this name: what if the identifier does not change (vary)?)
To get this, we introduce a new form into our language:
{* x x}}
We expect to be able to reduce this to:
by substituting 6 for x
in the body sub-expression of with
.
A little more complicated example:
{with {y {* x x}}
{+ y y}}}
[add] = {with {x 6} {with {y {* x x}} {+ y y}}}
[subst]= {with {y {* 6 6}} {+ y y}}
[mul] = {with {y 36} {+ y y}}
[subst]= {+ 36 36}
[add] = 72
Tuesday, January 21st WAE: Adding Bindings to AE
To add this to our language, we start with the BNF. We now call our language “WAE” (With+AE):
| { + <WAE> <WAE> }
| { - <WAE> <WAE> }
| { * <WAE> <WAE> }
| { / <WAE> <WAE> }
| { with { <id> <WAE> } <WAE> }
| <id>
Note that we had to introduce two new rules: one for introducing an
identifier, and one for using it. This is common in many language
specifications, for example define-type
introduces a new type, and it
comes with cases
that allows us to destruct its instances.
For <id>
we need to use some form of identifiers, the natural choice
in Racket is to use symbols. We can therefore write the corresponding
type definition:
[Num Number]
[Add WAE WAE]
[Sub WAE WAE]
[Mul WAE WAE]
[Div WAE WAE]
[Id Symbol]
[With Symbol WAE WAE])
The parser is easily extended to produce these syntax objects:
;; parses s-expressions into WAEs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
But note that this parser is inconvenient — if any of these expressions:
{foo 5 6}
{with x 5 {* x 8}}
{with {5 x} {* x 8}}
would result in a “bad syntax” error, which is not very helpful. To make
things better, we can add another case for with
expressions that are
malformed, and give a more specific message in that case:
;; parses s-expressions into WAEs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[(cons 'with more)
(error 'parse-sexpr "bad `with' syntax in ~s" sexpr)]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
and finally, to group all of the parsing code that deals with with
expressions (both valid and invalid ones), we can use a single case for
both of them:
;; parses s-expressions into WAEs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
;; go in here for all sexpr that begin with a 'with
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
And now we’re done with the syntactic part of the with
extension.
Quick note — why would we indent
With
like a normal function in code like this(With 'x
(Num 2)
(Add (Id 'x) (Num 4)))instead of an indentation that looks like a
let
(With 'x (Num 2)
(Add (Id 'x) (Num 4)))?
The reason for this is that the second indentation looks like a binding construct (eg, the indentation used in a
let
expression), butWith
is not a binding form — it’s a plain function because it’s at the Racket level. You should therefore keep in mind the huge difference between thatWith
and thewith
that appears in WAE programs:{with {x 2}
{+ x 4}}Another way to look at it: imagine that we intend for the language to be used by Spanish/Chinese/German/French speakers. In this case we would translate “
with
”:{con {x 2} {+ x 4}}
{he {x 2} {+ x 4}}
{mit {x 2} {+ x 4}}
{avec {x 2} {+ x 4}}
{c {x 2} {+ x 4}}but we will not do the same for
With
if we (the language implementors) are English speakers.
Tuesday, January 21st Evaluation of with
Now, to make this work, we will need to do some substitutions.
We basically want to say that to evaluate:
we need to evaluate WAE2
with id substituted by WAE1
. Formally:
= eval( subst(WAE2,id,WAE1) )
There is a more common syntax for substitution (quick: what do I mean by this use of “syntax”?):
= eval( WAE2[WAE1/id] )
Side-note: this syntax originates with logicians who used
[x/v]e
, and later there was a convention that mimicked the more natural order of arguments to a function withe[x->v]
, and eventually both of these got combined intoe[v/x]
which is a little confusing in that the left-to-right order of the arguments is not the same as for thesubst
function.
Now all we need is an exact definition of substitution.
Note that substitution is not the same as evaluation, it’s only a part of the evaluation process. In the previous examples, when we evaluated the expression we did substitutions as well as the usual arithmetic operations that were already part of the AE evaluator. In this last definition there is still a missing evaluation step, see if you can find it.
So let us try to define substitution now:
Substitution (take 1):
e[v/i]
To substitute an identifieri
in an expressione
with an expressionv
, replace all identifiers ine
that have the same namei
by the expressionv
.
This seems to work with simple expressions, for example:
{with {x 5} {+ 10 4}} --> {+ 10 4}
however, we crash with an invalid syntax if we try:
--> {+ 5 {with {5 3} 10}} ???
— we got to an invalid expression.
To fix this, we need to distinguish normal occurrences of identifiers, and ones that are used as new bindings. We need a few new terms for this:
-
Binding Instance: a binding instance of an identifier is one that is used to name it in a new binding. In our
<WAE>
syntax, binding instances are only the<id>
position of thewith
form. -
Scope: the scope of a binding instance is the region of program text in which instances of the identifier refer to the value bound in the binding instance. (Note that this definition actually relies on a definition of substitution, because that is what is used to specify how identifiers refer to values.)
-
Bound Instance (or Bound Occurrence / Identifier): an instance of an identifier is bound if it is contained within the scope of a binding instance of its name.
-
Free Instance (or Free Occurrence / Identifier): An identifier that is not contained in any binding instance of its name is said to be free.
Using this we can say that the problem with the previous definition of substitution is that it failed to distinguish between bound instances (which should be substituted) and binding instances (which should not). So we try to fix this:
Substitution (take 2):
e[v/i]
To substitute an identifieri
in an expressione
with an expressionv
, replace all instances ofi
that are not themselves binding instances with the expressionv
.
First of all, check the previous examples:
{with {x 5} {+ 10 4}} --> {+ 10 4}
still work, and
--> {+ 5 {with {x 3} 10}}
--> {+ 5 10}
also works. However, if we try this:
{+ x {with {x 3}
x}}}
we get:
--> {+ 5 5}
--> 10
but we want that to be 8
: the inner x
should be bound by the closest
with
that binds it.
The problem is that the new definition of substitution that we have
respects binding instances, but it fails to deal with their scope. In
the above example, we want the inner with
to shadow the outer
with
’s binding for x
.
Substitution (take 3):
e[v/i]
To substitute an identifieri
in an expressione
with an expressionv
, replace all instances ofi
that are not themselves binding instances, and that are not in any nested scope, with the expressionv
.
This avoids bad substitution above, but it is now doing things too carefully:
becomes
--> {+ 5 x}
which is an error because x
is unbound (and there is reasonable no
rule that we can specify to evaluate it).
The problem is that our substitution halts at every new scope, in this
case, it stopped at the new y
scope, but it shouldn’t have because it
uses a different name. In fact, that last definition of substitution
cannot handle any nested scope.
Revise again:
Substitution (take 4):
e[v/i]
To substitute an identifieri
in an expressione
with an expressionv
, replace all instances ofi
that are not themselves binding instances, and that are not in any nested scope ofi
, with the expressionv
.
which, finally, is a good definition. This is just a little too
mechanical. Notice that we actually refer to all instances of i
that
are not in a scope of a binding instance of i
, which simply means all
free occurrences of i
— free in e
(why? — remember the
definition of “free”?):
Substitution (take 4b):
e[v/i]
To substitute an identifieri
in an expressione
with an expressionv
, replace all instances ofi
that are free ine
with the expressionv
.
Based on this we can finally write the code for it:
;; substitutes the second argument with the third argument in the
;; first argument, as per the rules of substitution; the resulting
;; expression contains no free instances of the second argument
(define (subst expr from to) ; returns expr[to/from]
(cases expr
[(Num n) expr]
[(Add l r) (Add (subst l from to) (subst r from to))]
[(Sub l r) (Sub (subst l from to) (subst r from to))]
[(Mul l r) (Mul (subst l from to) (subst r from to))]
[(Div l r) (Div (subst l from to) (subst r from to))]
[(Id name) (if (eq? name from) to expr)]
[(With bound-id named-expr bound-body)
(if (eq? bound-id from)
expr ;*** don't go in!
(With bound-id
named-expr
(subst bound-body from to)))]))
… and this is just the same as writing a formal “paper version” of the substitution rule.
We still have bugs: but we’ll need some more work to get to them.
Before we find the bugs, we need to see when and how substitution is used in the evaluation process.
To modify our evaluator, we will need rules to deal with the new syntax
pieces — with
expressions and identifiers.
When we see an expression that looks like:
we continue by evaluating E1
to get a value V1
, we then substitute
the identifier x
with the expression V1
in E2
, and continue by
evaluating this new expression. In other words, we have the following
evaluation rule:
= eval( E2[eval(E1)/x] )
So we know what to do with with
expressions. How about identifiers?
The main feature of subst
, as said in the purpose statement, is that
it leaves no free instances of the substituted variable around. This
means that if the initial expression is valid (did not contain any free
variables), then when we go from
to
the result is an expression that has no free instances of x
. So we
don’t need to handle identifiers in the evaluator — substitutions make
them all go away.
We can now extend the formal definition of AE to that of WAE:
eval({with {x E1} E2}) = eval(E2[eval(E1)/x])
eval(id) = error!
If you’re paying close attention, you might catch a potential problem in
this definition: we’re substituting eval(E1)
for x
in E2
— an
operation that requires a WAE expression, but eval(E1)
is a number.
(Look at the type of the eval
definition we had for AE, then look at
the above definition of subst
.) This seems like being overly pedantic,
but we it will require some resolution when we get to the code. The
above rules are easily coded as follows:
;; evaluates WAE expressions by reducing them to numbers
(define (eval expr)
(cases expr
[(Num n) n]
[(Add l r) (+ (eval l) (eval r))]
[(Sub l r) (- (eval l) (eval r))]
[(Mul l r) (* (eval l) (eval r))]
[(Div l r) (/ (eval l) (eval r))]
[(With bound-id named-expr bound-body)
(eval (subst bound-body
bound-id
(Num (eval named-expr))))] ;***
[(Id name) (error 'eval "free identifier: ~s" name)]))
Note the Num
expression in the marked line: evaluating the named
expression gives us back a number — we need to convert this number
into a syntax to be able to use it with subst
. The solution is to use
Num
to convert the resulting number into a numeral (the syntax of a
number). It’s not an elegant solution, but it will do for now.
Finally, here are a few test cases. We use a new test
special form
which is part of the course plugin. The way to use test
is with two
expressions and an =>
arrow — DrRacket evaluates both, and nothing
will happen if the results are equal. If the results are different, you
will get a warning line, but evaluation will continue so you can try
additional tests. You can also use an =error>
arrow to test an error
message — use it with some text from the expected error, ?
stands
for any single character, and *
is a sequence of zero or more
characters. (When you use test
in your homework, the handin server
will abort when tests fail.) We expect these tests to succeed (make sure
that you understand why they should succeed).
(test (run "5") => 5)
(test (run "{+ 5 5}") => 10)
(test (run "{with {x {+ 5 5}} {+ x x}}") => 20)
(test (run "{with {x 5} {+ x x}}") => 10)
(test (run "{with {x {+ 5 5}} {with {y {- x 3}} {+ y y}}}") => 14)
(test (run "{with {x 5} {with {y {- x 3}} {+ y y}}}") => 4)
(test (run "{with {x 5} {+ x {with {x 3} 10}}}") => 15)
(test (run "{with {x 5} {+ x {with {x 3} x}}}") => 8)
(test (run "{with {x 5} {+ x {with {y 3} x}}}") => 10)
(test (run "{with {x 5} {with {y x} y}}") => 5)
(test (run "{with {x 5} {with {x x} x}}") => 5)
(test (run "{with {x 1} y}") =error> "free identifier")
Putting this all together, we get the following code; trying to run this code will raise an unexpected error…
#| BNF for the WAE language:
<WAE> ::= <num>
| { + <WAE> <WAE> }
| { - <WAE> <WAE> }
| { * <WAE> <WAE> }
| { / <WAE> <WAE> }
| { with { <id> <WAE> } <WAE> }
| <id>
|#
;; WAE abstract syntax trees
(define-type WAE
[Num Number]
[Add WAE WAE]
[Sub WAE WAE]
[Mul WAE WAE]
[Div WAE WAE]
[Id Symbol]
[With Symbol WAE WAE])
(: parse-sexpr : Sexpr -> WAE)
;; parses s-expressions into WAEs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
(: parse : String -> WAE)
;; parses a string containing a WAE expression to a WAE AST
(define (parse str)
(parse-sexpr (string->sexpr str)))
(: subst : WAE Symbol WAE -> WAE)
;; substitutes the second argument with the third argument in the
;; first argument, as per the rules of substitution; the resulting
;; expression contains no free instances of the second argument
(define (subst expr from to)
(cases expr
[(Num n) expr]
[(Add l r) (Add (subst l from to) (subst r from to))]
[(Sub l r) (Sub (subst l from to) (subst r from to))]
[(Mul l r) (Mul (subst l from to) (subst r from to))]
[(Div l r) (Div (subst l from to) (subst r from to))]
[(Id name) (if (eq? name from) to expr)]
[(With bound-id named-expr bound-body)
(if (eq? bound-id from)
expr
(With bound-id
named-expr
(subst bound-body from to)))]))
(: eval : WAE -> Number)
;; evaluates WAE expressions by reducing them to numbers
(define (eval expr)
(cases expr
[(Num n) n]
[(Add l r) (+ (eval l) (eval r))]
[(Sub l r) (- (eval l) (eval r))]
[(Mul l r) (* (eval l) (eval r))]
[(Div l r) (/ (eval l) (eval r))]
[(With bound-id named-expr bound-body)
(eval (subst bound-body
bound-id
(Num (eval named-expr))))]
[(Id name) (error 'eval "free identifier: ~s" name)]))
(: run : String -> Number)
;; evaluate a WAE program contained in a string
(define (run str)
(eval (parse str)))
;; tests
(test (run "5") => 5)
(test (run "{+ 5 5}") => 10)
(test (run "{with {x {+ 5 5}} {+ x x}}") => 20)
(test (run "{with {x 5} {+ x x}}") => 10)
(test (run "{with {x {+ 5 5}} {with {y {- x 3}} {+ y y}}}") => 14)
(test (run "{with {x 5} {with {y {- x 3}} {+ y y}}}") => 4)
(test (run "{with {x 5} {+ x {with {x 3} 10}}}") => 15)
(test (run "{with {x 5} {+ x {with {x 3} x}}}") => 8)
(test (run "{with {x 5} {+ x {with {y 3} x}}}") => 10)
(test (run "{with {x 5} {with {y x} y}}") => 5)
(test (run "{with {x 5} {with {x x} x}}") => 5)
(test (run "{with {x 1} y}") =error> "free identifier")
Oops, this program still has problems that were caught by the tests — we encounter unexpected free identifier errors. What’s the problem now? In expressions like:
{with {y x}
y}}
we forgot to substitute x
in the expression that y
is bound to. We
need to the recursive substitute in both the with’s body expression as
well as its named expression:
;; substitutes the second argument with the third argument in the
;; first argument, as per the rules of substitution; the resulting
;; expression contains no free instances of the second argument
(define (subst expr from to)
(cases expr
[(Num n) expr]
[(Add l r) (Add (subst l from to) (subst r from to))]
[(Sub l r) (Sub (subst l from to) (subst r from to))]
[(Mul l r) (Mul (subst l from to) (subst r from to))]
[(Div l r) (Div (subst l from to) (subst r from to))]
[(Id name) (if (eq? name from) to expr)]
[(With bound-id named-expr bound-body)
(if (eq? bound-id from)
expr
(With bound-id
(subst named-expr from to) ;*** new
(subst bound-body from to)))]))
And still we have a problem… Now it’s
{with {x x}
x}}
that halts with an error, but we want it to evaluate to 5
! Carefully
trying out our substitution code reveals the problem: when we substitute
5
for the outer x
, we don’t go inside the inner with
because it
has the same name — but we do need to go into its named expression.
We need to substitute in the named expression even if the identifier is
the same one we’re substituting:
;; substitutes the second argument with the third argument in the
;; first argument, as per the rules of substitution; the resulting
;; expression contains no free instances of the second argument
(define (subst expr from to)
(cases expr
[(Num n) expr]
[(Add l r) (Add (subst l from to) (subst r from to))]
[(Sub l r) (Sub (subst l from to) (subst r from to))]
[(Mul l r) (Mul (subst l from to) (subst r from to))]
[(Div l r) (Div (subst l from to) (subst r from to))]
[(Id name) (if (eq? name from) to expr)]
[(With bound-id named-expr bound-body)
(With bound-id
(subst named-expr from to)
(if (eq? bound-id from)
bound-body
(subst bound-body from to)))]))
The complete (and, finally, correct) version of the code is now:
wae.rkt D #lang pl
#| BNF for the WAE language:
<WAE> ::= <num>
| { + <WAE> <WAE> }
| { - <WAE> <WAE> }
| { * <WAE> <WAE> }
| { / <WAE> <WAE> }
| { with { <id> <WAE> } <WAE> }
| <id>
|#
;; WAE abstract syntax trees
(define-type WAE
[Num Number]
[Add WAE WAE]
[Sub WAE WAE]
[Mul WAE WAE]
[Div WAE WAE]
[Id Symbol]
[With Symbol WAE WAE])
(: parse-sexpr : Sexpr -> WAE)
;; parses s-expressions into WAEs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
(: parse : String -> WAE)
;; parses a string containing a WAE expression to a WAE AST
(define (parse str)
(parse-sexpr (string->sexpr str)))
#| Formal specs for `subst':
(`N' is a <num>, `E1', `E2' are <WAE>s, `x' is some <id>,
`y' is a *different* <id>)
N[v/x] = N
{+ E1 E2}[v/x] = {+ E1[v/x] E2[v/x]}
{- E1 E2}[v/x] = {- E1[v/x] E2[v/x]}
{* E1 E2}[v/x] = {* E1[v/x] E2[v/x]}
{/ E1 E2}[v/x] = {/ E1[v/x] E2[v/x]}
y[v/x] = y
x[v/x] = v
{with {y E1} E2}[v/x] = {with {y E1[v/x]} E2[v/x]}
{with {x E1} E2}[v/x] = {with {x E1[v/x]} E2}
|#
(: subst : WAE Symbol WAE -> WAE)
;; substitutes the second argument with the third argument in the
;; first argument, as per the rules of substitution; the resulting
;; expression contains no free instances of the second argument
(define (subst expr from to)
(cases expr
[(Num n) expr]
[(Add l r) (Add (subst l from to) (subst r from to))]
[(Sub l r) (Sub (subst l from to) (subst r from to))]
[(Mul l r) (Mul (subst l from to) (subst r from to))]
[(Div l r) (Div (subst l from to) (subst r from to))]
[(Id name) (if (eq? name from) to expr)]
[(With bound-id named-expr bound-body)
(With bound-id
(subst named-expr from to)
(if (eq? bound-id from)
bound-body
(subst bound-body from to)))]))
#| Formal specs for `eval':
eval(N) = N
eval({+ E1 E2}) = eval(E1) + eval(E2)
eval({- E1 E2}) = eval(E1) - eval(E2)
eval({* E1 E2}) = eval(E1) * eval(E2)
eval({/ E1 E2}) = eval(E1) / eval(E2)
eval(id) = error!
eval({with {x E1} E2}) = eval(E2[eval(E1)/x])
|#
(: eval : WAE -> Number)
;; evaluates WAE expressions by reducing them to numbers
(define (eval expr)
(cases expr
[(Num n) n]
[(Add l r) (+ (eval l) (eval r))]
[(Sub l r) (- (eval l) (eval r))]
[(Mul l r) (* (eval l) (eval r))]
[(Div l r) (/ (eval l) (eval r))]
[(With bound-id named-expr bound-body)
(eval (subst bound-body
bound-id
(Num (eval named-expr))))]
[(Id name) (error 'eval "free identifier: ~s" name)]))
(: run : String -> Number)
;; evaluate a WAE program contained in a string
(define (run str)
(eval (parse str)))
;; tests
(test (run "5") => 5)
(test (run "{+ 5 5}") => 10)
(test (run "{with {x 5} {+ x x}}") => 10)
(test (run "{with {x {+ 5 5}} {+ x x}}") => 20)
(test (run "{with {x 5} {with {y {- x 3}} {+ y y}}}") => 4)
(test (run "{with {x {+ 5 5}} {with {y {- x 3}} {+ y y}}}") => 14)
(test (run "{with {x 5} {+ x {with {x 3} 10}}}") => 15)
(test (run "{with {x 5} {+ x {with {x 3} x}}}") => 8)
(test (run "{with {x 5} {+ x {with {y 3} x}}}") => 10)
(test (run "{with {x 5} {with {y x} y}}") => 5)
(test (run "{with {x 5} {with {x x} x}}") => 5)
(test (run "{with {x 1} y}") =error> "free identifier")
Tuesday, January 21st Evaluation of with
(contd.)
Reminder:
-
We started doing substitution, with a
let
-like form:with
. -
Reasons for using bindings:
- Avoid writing expressions twice.
- More expressive language (can express identity).
- Duplicating is bad! (“DRY”: Don’t Repeat Yourself.)
- Avoids static redundancy.
- Avoid redundant computations.
- More than just an optimization when it avoids exponential resources.
- Avoids dynamic redundancy.
- Avoid writing expressions twice.
-
BNF:
<WAE> ::= <num>
| { + <WAE> <WAE> }
| { - <WAE> <WAE> }
| { * <WAE> <WAE> }
| { / <WAE> <WAE> }
| { with { <id> <WAE> } <WAE> }
| <id>Note that we had to introduce two new rules: one for introducing an identifier, and one for using it.
-
Type definition:
(define-type WAE
[Num Number]
[Add WAE WAE]
[Sub WAE WAE]
[Mul WAE WAE]
[Div WAE WAE]
[Id Symbol]
[With Symbol WAE WAE]) -
Parser:
(: parse-sexpr : Sexpr -> WAE)
;; parses s-expressions into WAEs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s"
sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)])) -
We need to define substitution. Terms:
- Binding Instance.
- Scope.
- Bound Instance.
- Free Instance.
-
After lots of attempts:
e[v/i] — To substitute an identifier
i
in an expressione
with an expressionv
, replace all instances ofi
that are free ine
with the expressionv
. -
Implemented the code, and again, needed to fix a few bugs:
(: subst : WAE Symbol WAE -> WAE)
;; substitutes the second argument with the third argument in the
;; first argument, as per the rules of substitution; the resulting
;; expression contains no free instances of the second argument
(define (subst expr from to)
(cases expr
[(Num n) expr]
[(Add l r) (Add (subst l from to) (subst r from to))]
[(Sub l r) (Sub (subst l from to) (subst r from to))]
[(Mul l r) (Mul (subst l from to) (subst r from to))]
[(Div l r) (Div (subst l from to) (subst r from to))]
[(Id name) (if (eq? name from) to expr)]
[(With bound-id named-expr bound-body)
(With bound-id
(subst named-expr from to)
(if (eq? bound-id from)
bound-body
(subst bound-body from to)))]))(Note that the bugs that we fixed clarify the exact way that our scopes work: in
{with {x 2} {with {x {+ x 2}} x}}
, the scope of the firstx
is the{+ x 2}
expression.) -
We then extended the AE evaluation rules:
eval(...) = ... same as the AE rules ...
eval({with {x E1} E2}) = eval(E2[eval(E1)/x])
eval(id) = error!and noted the possible type problem.
-
The above translated into a Racket definition for an
eval
function (with a hack to avoid the type issue):(: eval : WAE -> Number)
;; evaluates WAE expressions by reducing them to numbers
(define (eval expr)
(cases expr
[(Num n) n]
[(Add l r) (+ (eval l) (eval r))]
[(Sub l r) (- (eval l) (eval r))]
[(Mul l r) (* (eval l) (eval r))]
[(Div l r) (/ (eval l) (eval r))]
[(With bound-id named-expr bound-body)
(eval (subst bound-body
bound-id
(Num (eval named-expr))))]
[(Id name) (error 'eval "free identifier: ~s" name)]))
Tuesday, January 21st Formal Specs
Note the formal definitions that were included in the WAE code. They are ways of describing pieces of our language that are more formal than plain English, but still not as formal (and as verbose) as the actual code.
A formal definition of subst
:
(N
is a <num>
, E1
, E2
are <WAE>
s, x
is some <id>
, y
is a
different <id>
)
{+ E1 E2}[v/x] = {+ E1[v/x] E2[v/x]}
{- E1 E2}[v/x] = {- E1[v/x] E2[v/x]}
{* E1 E2}[v/x] = {* E1[v/x] E2[v/x]}
{/ E1 E2}[v/x] = {/ E1[v/x] E2[v/x]}
y[v/x] = y
x[v/x] = v
{with {y E1} E2}[v/x] = {with {y E1[v/x]} E2[v/x]}
{with {x E1} E2}[v/x] = {with {x E1[v/x]} E2}
And a formal definition of eval
:
eval({+ E1 E2}) = eval(E1) + eval(E2)
eval({- E1 E2}) = eval(E1) - eval(E2)
eval({* E1 E2}) = eval(E1) * eval(E2)
eval({/ E1 E2}) = eval(E1) / eval(E2)
eval(id) = error!
eval({with {x E1} E2}) = eval(E2[eval(E1)/x])
Tuesday, January 21st Lazy vs Eager Evaluation
As we have previously seen, there are two basic approaches for evaluation: either eager or lazy. In lazy evaluation, bindings are used for sort of textual references — it is only for avoiding writing an expression twice, but the associated computation is done twice anyway. In eager evaluation, we eliminate not only the textual redundancy, but also the computation.
Which evaluation method did our evaluator use? The relevant piece of
formalism is the treatment of with
:
And the matching piece of code is:
(eval (subst bound-body
bound-id
(Num (eval named-expr))))]
How do we make this lazy?
In the formal equation:
and in the code:
;; evaluates WAE expressions by reducing them to numbers
(define (eval expr)
(cases expr
[(Num n) n]
[(Add l r) (+ (eval l) (eval r))]
[(Sub l r) (- (eval l) (eval r))]
[(Mul l r) (* (eval l) (eval r))]
[(With bound-id named-expr bound-body)
(eval (subst bound-body
bound-id
named-expr))] ;*** no eval and no Num wrapping
[(Id name) (error 'eval "free identifier: ~s" name)]))
We can verify the way this works by tracing eval
(compare the trace
you get for the two versions):
> (run "{with {x {+ 1 2}} {* x x}}")
Ignoring the traces for now, the modified WAE interpreter works as before, specifically, all tests pass. So the question is whether the language we get is actually different than the one we had before. One difference is in execution speed, but we can’t really notice a difference, and we care more about meaning. Is there any program that will run differently in the two languages?
The main feature of the lazy evaluator is that it is not evaluating the named expression until it is actually needed. As we have seen, this leads to duplicating computations if the bound identifier is used more than once — meaning that it does not eliminate the dynamic redundancy. But what if the bound identifier is not used at all? In that case the named expression simply evaporates. This is a good hint at an expression that behaves differently in the two languages — if we add division to both languages, we get a different result when we try running:
The eager evaluator stops with an error when it tries evaluating the division — and the lazy evaluator simply ignores it.
Even without division, we get a similar behavior for
but it is questionable whether the fact that this evaluates to 7 is correct behavior — we really want to forbid program that use free variable.
Furthermore, there is an issue with name capturing — we don’t want to substitute an expression into a context that captures some of its free variables. But our substitution allows just that, which is usually not a problem because by the time we do the substitution, the named expression should not have free variables that need to be replaced. However, consider evaluating this program:
{with {x 2}
{+ x y}}}
under the two evaluation regimens: the eager version stops with an error, and the lazy version succeed. This points at a bug in our substitution, or rather not dealing with an issue that we do not encounter.
So the summary is: as long as the initial program is correct, both evaluation regimens produce the same results. If a program contains free variables, they might get captured in a naive lazy evaluator implementation (but this is a bug that should be fixed). Also, there are some cases where eager evaluation runs into a run-time problem which does not happen in a lazy evaluator because the expression is not used. It is possible to prove that when you evaluate an expression, if there is an error that can be avoided, lazy evaluation will always avoid it, whereas an eager evaluator will always run into it. On the other hand, lazy evaluators are usually slower than eager evaluator, so it’s a speed vs. robustness trade-off.
Note that with lazy evaluation we say that an identifier is bound to an
expression rather than a value. (Again, this is why the eager version
needed to wrap eval
’s result in a Num
and this one doesn’t.)
(It is possible to change things and get a more well behaved substitution, we basically will need to find if a capture might happen, and rename things to avoid it. For example,
if `x' and `y' are equal
= {with {y E1[v/x]} E2} = {with {x E1[v/x]} E2}
if `y' has a free occurrence in `v'
= {with {y1 E1[v/x]} E2[y1/y][v/x]} ; `y1' is "fresh"
otherwise
= {with {y E1[v/x]} E2[v/x]}
With this, we might have gone through this path in evaluating the above:
{with {x₁ 2} {+ x₁ x}} ; note that x₁ is a fresh name, not x
{+ 2 x}
error: free `x`
But you can see that this is much more complicated (more code: requires
a free-in
predicate, being able to invent new fresh names, etc). And
it’s not even the end of that story…)
Tuesday, January 21st de Bruijn Indexes
This whole story revolves around names, specifically, name capture is a problem that should always be avoided (it is one major source of PL headaches).
But are names the only way we can use bindings?
There is a least one alternative way: note that the only thing we used names for are for references. We don’t really care what the name is, which is pretty obvious when we consider the two WAE expressions:
{with {y 5} {+ y y}}
or the two Racket function definitions:
(define (foo y) (list y y))
Both of these show a pair of expressions that we should consider as
equal in some sense (this is called “alpha-equality”). The only thing we
care about is what variable points where: the binding structure is the
only thing that matters. In other words, as long as DrRacket produces
the same arrows when we use Check Syntax, we consider the program to be
the same, regardless of name choices (for argument names and local
names, not for global names like foo
in the above).
The alternative idea uses this principle: if all we care about is where the arrows go, then simply get rid of the names… Instead of referencing a binding through its name, just specify which of the surrounding scopes we want to refer to. For example, instead of:
we can use a new “reference” syntax — [N]
— and use this instead
of the above:
So the rules for [N]
are — [0]
is the value bound in the current
scope, [1]
is the value from the next one up etc.
Of course, to do this translation, we have to know the precise scope rules. Two more complicated examples:
is translated to:
(note how x
appears as a different reference based on where it
appeared in the original code.) Even more subtle:
is translated to:
because the inner with
does not have its own named expression in its
scope, so the named expression is immediately in the scope of the outer
with
.
This is called “de Bruijn Indexes”: instead of referencing identifiers by their name, we use an index into the surrounding binding context. The major disadvantage, as can be seen in the above examples, is that it is not convenient for humans to work with. Specifically, the same identifier is referenced using different numbers, which makes it hard to understand what some code is doing. After all, abstractions are the main thing we deal with when we write programs, and having labels make the bindings structure much easier to understand than scope counts.
However, practically all compilers use this for compiled code (think about stack pointers). For example, GCC compiles this code:
int x = 5;
{
int y = x + 1;
return x + y;
}
}
to:
movl $5, -4(%ebp) ; int x = 5
movl -4(%ebp), %eax
incl %eax
movl %eax, -8(%ebp) ; int y = %eax
movl -8(%ebp), %eax
addl -4(%ebp), %eax
Tuesday, January 28th Functions & Function Values
Now that we have a form for local bindings, which forced us to deal with
proper substitutions and everything that is related, we can get to
functions. The concept of a function is itself very close to
substitution, and to our with
form. For example, when we write:
{* x x}}
then the {* x x}
body is itself parametrized over some value for x
.
If we take this expression and take out the 5
, we’re left with
something that has all of the necessary ingredients of a function — a
bunch of code that is parameterized over some input identifier:
{* x x}}
We only need to replace with
and use a proper name that indicates that
it’s a function:
{* x x}}
Now we have a new form in our language, one that should have a function
as its meaning. As we have seen in the case of with
expressions, we
also need a new form to use these functions. We will use call
for
this, so that
5}
will be the same as the original with
expression that we started with
— the fun
expression is like the with
expression with no value,
and applying it on 5
is providing that value back:
{* x x}}
Of course, this does not help much — all we get is a way to use local
bindings that is more verbose from what we started with. What we’re
really missing is a way to name these functions. If we get the right
evaluation rules, we can evaluate a fun
expression to some value —
which will allow us to bind it to a variable using with
. Something
like this:
{+ {call sqr 5}
{call sqr 6}}}
In this expression, we say that x
is the formal parameter (or
argument), and the 5
and 6
are actual parameters (sometimes
abbreviated as formals and actuals). Note that naming functions often
helps, but many times there are small functions that are fine to specify
without a name — for example, consider a two-stage addition function,
where there is no apparent good name for the returned function:
{fun {y}
{+ x y}}}}
{call {call add 8} 9}}
Tuesday, January 28th Implementing First Class Functions
This is a simple plan, but it is directly related to how functions are
going to be used in our language. We know that {call {fun {x} E1} E2}
is equivalent to a with
expression, but the new thing here is that we
do allow writing just the {fun ...}
expression by itself, and
therefore we need to have some meaning for it. The meaning, or the value
of this expression, should roughly be “an expression that needs a value
to be plugged in for x
”. In other words, our language will have these
new kinds of values that contain an expression to be evaluated later on.
There are three basic approaches that classify programming languages in relation to how the deal with functions:
-
First order: functions are not real values. They cannot be used or returned as values by other functions. This means that they cannot be stored in data structures. This is what most “conventional” languages used to have in the past. (You will be implementing such a language in homework 4.)
An example of such a language is the Beginner Student language that is used in HtDP, where the language is intentionally first-order to help students write correct code (at the early stages where using a function as a value is usually an error). It’s hard to find practical modern languages that fall in this category.
-
Higher order: functions can receive and return other functions as values. This is what you get with C and modern Fortran.
-
First class: functions are values with all the rights of other values. In particular, they can be supplied to other functions, returned from functions, stored in data structures, and new functions can be created at run-time. (And most modern languages have first class functions.)
The last category is the most interesting one. Back in the old days, complex expressions were not first-class in that they could not be freely composed. This is still the case in machine-code: as we’ve seen earlier, to compute an expression such as
you have to do something like this:
y = 4 * a
y = y * c
x = x - y
x = sqrt(x)
y = -b
x = y + x
y = 2 * a
s = x / y
In other words, every intermediate value needs to have its own name. But with proper (“high-level”) programming languages (at least most of them…) you can just write the original expression, with no names for these values.
With first-class functions something similar happens — it is possible to have complex expressions that consume and return functions, and they do not need to be named.
What we get with our fun
expression (if we can make it work) is
exactly this: it generates a function, and you can choose to either bind
it to a name, or not. The important thing is that the value exists
independently of a name.
This has a major effect on the “personality” of a programming language as we will see. In fact, just adding this feature will make our language much more advanced than languages with just higher-order or first-order functions.
Quick Example: the following is working JavaScript code, that uses first class functions.
function bar(y) { return x + y; }
return bar;
}
function main() {
var f = foo(1);
var g = foo(10);
return [f(2), g(2)];
}
Note that the above definition of foo
does not use an anonymous
“lambda expression” — in Racket terms, it’s translated to
(define (bar y) (+ x y))
bar)
The returned function is not anonymous, but it’s not really named
either: the bar
name is bound only inside the body of foo
, and
outside of it that name no longer exists since it’s not its scope. It
gets used in the printed form if the function value is displayed, but
this is merely a debugging aid. The anonymous lambda
version that is
common in Racket can be used in JavaScript too:
return function(y) { return x + y; }
}
Side-note: GCC includes extensions that allow internal function definitions, but it still does not have first class functions — trying to do the above is broken:
#include <stdio.h>
typedef int(*int2int)(int);
int2int foo(int x) {
int bar(int y) { return x + y; }
return bar;
}
int main() {
int2int f = foo(1);
int2int g = foo(10);
printf(">> %d, %d\n", f(2), g(2));
}
Tuesday, January 28th Side-note: how important is it to have anonymous functions?
You’ll see many places where people refer to the feature of first-class functions as the ability to create anonymous functions, but this is a confusion and it’s not accurate. Whether a function has a name or not is not the important question — instead, the important question is whether functions can exist with no bindings that refers to them.
As a quick example in Racket:
(define (bar y) (+ x y))
bar)
in Javascript:
function bar(y) {
return x + y;
}
return bar;
}
and in Python:
def bar(y):
return x + y
return bar
In all three of these, we have a foo
function that returns a function
named bar
— but the bar
name, is only available in the scope of
foo
. The fact that the name is displayed as part of the textual
rendering of the function value is merely a debugging feature.
Tuesday, January 28th The FLANG Language
Now for the implementation — we call this new language FLANG.
First, the BNF:
| { + <FLANG> <FLANG> }
| { - <FLANG> <FLANG> }
| { * <FLANG> <FLANG> }
| { / <FLANG> <FLANG> }
| { with { <id> <FLANG> } <FLANG> }
| <id>
| { fun { <id> } <FLANG> }
| { call <FLANG> <FLANG> }
And the matching type definition:
[Num Number]
[Add FLANG FLANG]
[Sub FLANG FLANG]
[Mul FLANG FLANG]
[Div FLANG FLANG]
[Id Symbol]
[With Symbol FLANG FLANG]
[Fun Symbol FLANG] ; No named-expression
[Call FLANG FLANG])
The parser for this grammar is, as usual, straightforward:
;; parses s-expressions into FLANGs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])]
[(cons 'fun more)
(match sexpr
[(list 'fun (list (symbol: name)) body)
(Fun name (parse-sexpr body))]
[else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[(list 'call fun arg)
(Call (parse-sexpr fun) (parse-sexpr arg))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
We also need to patch up the substitution function to deal with these
things. The scoping rule for the new function form is, unsurprisingly,
similar to the rule of with
, except that there is no extra expression
now, and the scoping rule for call
is the same as for the arithmetic
operators:
{+ E1 E2}[v/x] = {+ E1[v/x] E2[v/x]}
{- E1 E2}[v/x] = {- E1[v/x] E2[v/x]}
{* E1 E2}[v/x] = {* E1[v/x] E2[v/x]}
{/ E1 E2}[v/x] = {/ E1[v/x] E2[v/x]}
y[v/x] = y
x[v/x] = v
{with {y E1} E2}[v/x] = {with {y E1[v/x]} E2[v/x]}
{with {x E1} E2}[v/x] = {with {x E1[v/x]} E2}
{call E1 E2}[v/x] = {call E1[v/x] E2[v/x]}
{fun {y} E}[v/x] = {fun {y} E[v/x]}
{fun {x} E}[v/x] = {fun {x} E}
And the matching code:
;; substitutes the second argument with the third argument in the
;; first argument, as per the rules of substitution; the resulting
;; expression contains no free instances of the second argument
(define (subst expr from to)
(cases expr
[(Num n) expr]
[(Add l r) (Add (subst l from to) (subst r from to))]
[(Sub l r) (Sub (subst l from to) (subst r from to))]
[(Mul l r) (Mul (subst l from to) (subst r from to))]
[(Div l r) (Div (subst l from to) (subst r from to))]
[(Id name) (if (eq? name from) to expr)]
[(With bound-id named-expr bound-body)
(With bound-id
(subst named-expr from to)
(if (eq? bound-id from)
bound-body
(subst bound-body from to)))]
[(Call l r) (Call (subst l from to) (subst r from to))]
[(Fun bound-id bound-body)
(if (eq? bound-id from)
expr
(Fun bound-id (subst bound-body from to)))]))
Now, before we start working on an evaluator, we need to decide on what exactly do we use to represent values of this language. Before we had functions, we had only number values and we used Racket numbers to represent them. Now we have two kinds of values — numbers and functions. It seems easy enough to continue using Racket numbers to represent numbers, but what about functions? What should be the result of evaluating
? Well, this is the new toy we have: it should be a function value,
which is something that can be used just like numbers, but instead of
arithmetic operations, we can call
these things. What we need is a way
to avoid evaluating the body expression of the function — delay it
— and instead use some value that will contain this delayed expression
in a way that can be used later.
To accommodate this, we will change our implementation strategy a
little: we will use our syntax objects for numbers ((Num n)
instead of
just n
), which will be a little inconvenient when we do the arithmetic
operations, but it will simplify life by making it possible to evaluate
functions in a similar way: simply return their own syntax object as
their values. The syntax object has what we need: the body expression
that needs to be evaluated later when the function is called, and it
also has the identifier name that should be replaced with the actual
input to the function call. This means that evaluating:
now yields
and a number (Num 5)
evaluates to (Num 5)
.
In a similar way, (Fun 'x (Num 2))
evaluates to (Fun 'x (Num 2))
.
Why would this work? Well, because call
will be very similar to with
— the only difference is that its arguments are ordered a little
differently, being retrieved from the function that is applied and the
argument.
The formal evaluation rules are therefore treating functions like numbers, and use the syntax object to represent both values:
eval({+ E1 E2}) = eval(E1) + eval(E2)
eval({- E1 E2}) = eval(E1) - eval(E2)
eval({* E1 E2}) = eval(E1) * eval(E2)
eval({/ E1 E2}) = eval(E1) / eval(E2)
eval(id) = error!
eval({with {x E1} E2}) = eval(E2[eval(E1)/x])
eval(FUN) = FUN ; assuming FUN is a function expression
eval({call E1 E2})
= eval(B[eval(E2)/x]) if eval(E1) = {fun {x} B}
= error! otherwise
Note that the last rule could be written using a translation to a with
expression:
= eval({with {x E2} B}) if eval(E1) = {fun {x} B}
= error! otherwise
And alternatively, we could specify with
using call
and fun
:
There is a small problem in these rules which is intuitively seen by the
fact that the evaluation rule for a call
is expected to be very
similar to the one for arithmetic operations. We now have two kinds of
values, so we need to check the arithmetic operation’s arguments too:
if eval(E1), eval(E2) evaluate to numbers N1, N2
otherwise error!
...
The corresponding code is:
;; evaluates FLANG expressions by reducing them to *expressions* but
;; only expressions that stand for values: only `Fun`s and `Num`s
(define (eval expr)
(cases expr
[(Num n) expr] ;*** change here
[(Add l r) (arith-op + (eval l) (eval r))]
[(Sub l r) (arith-op - (eval l) (eval r))]
[(Mul l r) (arith-op * (eval l) (eval r))]
[(Div l r) (arith-op / (eval l) (eval r))]
[(With bound-id named-expr bound-body)
(eval (subst bound-body
bound-id
(eval named-expr)))] ;*** no `(Num ...)'
[(Id name) (error 'eval "free identifier: ~s" name)]
[(Fun bound-id bound-body) expr] ;*** similar to `Num'
[(Call (Fun bound-id bound-body) arg-expr) ;*** nested pattern
(eval (subst bound-body ;*** just like `with'
bound-id
(eval arg-expr)))]
[(Call something arg-expr)
(error 'eval "`call' expects a function, got: ~s" something)]))
Note that the Call
case is doing the same thing we do in the With
case. In fact, we could have just generated a With
expression and
evaluate that instead:
[(Call (Fun bound-id bound-body) arg-expr)
(eval (With bound-id arg-expr bound-body))]
...
The arith-op
function is in charge of checking that the input values
are numbers (represented as FLANG numbers), translating them to plain
numbers, performing the Racket operation, then re-wrapping the result in
a Num
. Note how its type indicates that it is a higher-order function.
;; gets a Racket numeric binary operator, and uses it within a FLANG
;; `Num' wrapper (note the H.O. type, and note the hack of the `val`
;; name which is actually an AST that represents a runtime value)
(define (arith-op op val1 val2)
(Num (op (Num->number val1) (Num->number val2))))
It uses the following function to convert FLANG numbers to Racket
numbers. (Note that else
is almost always a bad idea since it can
prevent the compiler from showing you places to edit code — but this
case is an exception since we never want to deal with anything other
than Num
s.) The reason that this function is relatively trivial is
that we chose the easy way and represented numbers using Racket numbers,
but we could have used strings or anything else.
;; convert a FLANG number to a Racket one
(define (Num->number e)
(cases e
[(Num n) n]
[else (error 'arith-op "expected a number, got: ~s" e)]))
We can also make things a little easier to use if we make run
convert
the result to a number:
;; evaluate a FLANG program contained in a string
(define (run str)
(let ([result (eval (parse str))])
(cases result
[(Num n) n]
[else (error 'run "evaluation returned a non-number: ~s"
result)])))
Adding few simple tests we get:
#lang pl
#|
The grammar:
<FLANG> ::= <num>
| { + <FLANG> <FLANG> }
| { - <FLANG> <FLANG> }
| { * <FLANG> <FLANG> }
| { / <FLANG> <FLANG> }
| { with { <id> <FLANG> } <FLANG> }
| <id>
| { fun { <id> } <FLANG> }
| { call <FLANG> <FLANG> }
Evaluation rules:
subst:
N[v/x] = N
{+ E1 E2}[v/x] = {+ E1[v/x] E2[v/x]}
{- E1 E2}[v/x] = {- E1[v/x] E2[v/x]}
{* E1 E2}[v/x] = {* E1[v/x] E2[v/x]}
{/ E1 E2}[v/x] = {/ E1[v/x] E2[v/x]}
y[v/x] = y
x[v/x] = v
{with {y E1} E2}[v/x] = {with {y E1[v/x]} E2[v/x]} ; if y =/= x
{with {x E1} E2}[v/x] = {with {x E1[v/x]} E2}
{call E1 E2}[v/x] = {call E1[v/x] E2[v/x]}
{fun {y} E}[v/x] = {fun {y} E[v/x]} ; if y =/= x
{fun {x} E}[v/x] = {fun {x} E}
eval:
eval(N) = N
eval({+ E1 E2}) = eval(E1) + eval(E2) \ if both E1 and E2
eval({- E1 E2}) = eval(E1) - eval(E2) \ evaluate to numbers
eval({* E1 E2}) = eval(E1) * eval(E2) / otherwise error!
eval({/ E1 E2}) = eval(E1) / eval(E2) /
eval(id) = error!
eval({with {x E1} E2}) = eval(E2[eval(E1)/x])
eval(FUN) = FUN ; assuming FUN is a function expression
eval({call E1 E2}) = eval(B[eval(E2)/x])
if eval(E1)={fun {x} B}, otherwise error!
|#
(define-type FLANG
[Num Number]
[Add FLANG FLANG]
[Sub FLANG FLANG]
[Mul FLANG FLANG]
[Div FLANG FLANG]
[Id Symbol]
[With Symbol FLANG FLANG]
[Fun Symbol FLANG]
[Call FLANG FLANG])
(: parse-sexpr : Sexpr -> FLANG)
;; parses s-expressions into FLANGs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])]
[(cons 'fun more)
(match sexpr
[(list 'fun (list (symbol: name)) body)
(Fun name (parse-sexpr body))]
[else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[(list 'call fun arg)
(Call (parse-sexpr fun) (parse-sexpr arg))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
(: parse : String -> FLANG)
;; parses a string containing a FLANG expression to a FLANG AST
(define (parse str)
(parse-sexpr (string->sexpr str)))
(: subst : FLANG Symbol FLANG -> FLANG)
;; substitutes the second argument with the third argument in the
;; first argument, as per the rules of substitution; the resulting
;; expression contains no free instances of the second argument
(define (subst expr from to)
(cases expr
[(Num n) expr]
[(Add l r) (Add (subst l from to) (subst r from to))]
[(Sub l r) (Sub (subst l from to) (subst r from to))]
[(Mul l r) (Mul (subst l from to) (subst r from to))]
[(Div l r) (Div (subst l from to) (subst r from to))]
[(Id name) (if (eq? name from) to expr)]
[(With bound-id named-expr bound-body)
(With bound-id
(subst named-expr from to)
(if (eq? bound-id from)
bound-body
(subst bound-body from to)))]
[(Call l r) (Call (subst l from to) (subst r from to))]
[(Fun bound-id bound-body)
(if (eq? bound-id from)
expr
(Fun bound-id (subst bound-body from to)))]))
(: Num->number : FLANG -> Number)
;; convert a FLANG number to a Racket one
(define (Num->number e)
(cases e
[(Num n) n]
[else (error 'arith-op "expected a number, got: ~s" e)]))
(: arith-op : (Number Number -> Number) FLANG FLANG -> FLANG)
;; gets a Racket numeric binary operator, and uses it within a FLANG
;; `Num' wrapper
(define (arith-op op val1 val2)
(Num (op (Num->number val1) (Num->number val2))))
(: eval : FLANG -> FLANG)
;; evaluates FLANG expressions by reducing them to *expressions* but
;; only expressions that stand for values: only `Fun`s and `Num`s
(define (eval expr)
(cases expr
[(Num n) expr]
[(Add l r) (arith-op + (eval l) (eval r))]
[(Sub l r) (arith-op - (eval l) (eval r))]
[(Mul l r) (arith-op * (eval l) (eval r))]
[(Div l r) (arith-op / (eval l) (eval r))]
[(With bound-id named-expr bound-body)
(eval (subst bound-body
bound-id
(eval named-expr)))]
[(Id name) (error 'eval "free identifier: ~s" name)]
[(Fun bound-id bound-body) expr]
[(Call (Fun bound-id bound-body) arg-expr)
(eval (subst bound-body
bound-id
(eval arg-expr)))]
[(Call something arg-expr)
(error 'eval "`call' expects a function, got: ~s" something)]))
(: run : String -> Number)
;; evaluate a FLANG program contained in a string
(define (run str)
(let ([result (eval (parse str))])
(cases result
[(Num n) n]
[else (error 'run "evaluation returned a non-number: ~s"
result)])))
;; tests
(test (run "{call {fun {x} {+ x 1}} 4}")
=> 5)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{call add3 1}}")
=> 4)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{with {add1 {fun {x} {+ x 1}}}
{with {x 3}
{call add1 {call add3 x}}}}}")
=> 7)
Tuesday, January 28th The FLANG Language (contd.)
There is still a problem with this version. First a question — if
call
is similar to arithmetic operations (and to with
in what it
actually does), then how come the code is different enough that it
doesn’t even need an auxiliary function?
Second question: what should happen if we evaluate these code snippets:
{fun {y}
{+ x y}}}}
{call {call add 8} 9}}")
(run "{with {identity {fun {x} x}}
{with {foo {fun {x} {+ x 1}}}
{call {call identity foo} 123}}}")
(run "{call {call {fun {x} {call x 1}}
{fun {x} {fun {y} {+ x y}}}}
123}")
Third question, what will happen if we do the above?
What we’re missing is an evaluation of the function expression, in case
it’s not a literal fun
form. The following fixes this:
;; evaluates FLANG expressions by reducing them to *expressions* but
;; only expressions that stand for values: only `Fun`s and `Num`s
(define (eval expr)
(cases expr
[(Num n) expr]
[(Add l r) (arith-op + (eval l) (eval r))]
[(Sub l r) (arith-op - (eval l) (eval r))]
[(Mul l r) (arith-op * (eval l) (eval r))]
[(Div l r) (arith-op / (eval l) (eval r))]
[(With bound-id named-expr bound-body)
(eval (subst bound-body
bound-id
(eval named-expr)))]
[(Id name) (error 'eval "free identifier: ~s" name)]
[(Fun bound-id bound-body) expr]
[(Call fun-expr arg-expr)
(define fval (eval fun-expr)) ;*** need to evaluate this!
(cases fval
[(Fun bound-id bound-body)
(eval (subst bound-body bound-id (eval arg-expr)))]
[else (error 'eval "`call' expects a function, got: ~s"
fval)])]))
The complete code is:
flang.rkt D ;; The Flang interpreter
#lang pl
#|
The grammar:
<FLANG> ::= <num>
| { + <FLANG> <FLANG> }
| { - <FLANG> <FLANG> }
| { * <FLANG> <FLANG> }
| { / <FLANG> <FLANG> }
| { with { <id> <FLANG> } <FLANG> }
| <id>
| { fun { <id> } <FLANG> }
| { call <FLANG> <FLANG> }
Evaluation rules:
subst:
N[v/x] = N
{+ E1 E2}[v/x] = {+ E1[v/x] E2[v/x]}
{- E1 E2}[v/x] = {- E1[v/x] E2[v/x]}
{* E1 E2}[v/x] = {* E1[v/x] E2[v/x]}
{/ E1 E2}[v/x] = {/ E1[v/x] E2[v/x]}
y[v/x] = y
x[v/x] = v
{with {y E1} E2}[v/x] = {with {y E1[v/x]} E2[v/x]} ; if y =/= x
{with {x E1} E2}[v/x] = {with {x E1[v/x]} E2}
{call E1 E2}[v/x] = {call E1[v/x] E2[v/x]}
{fun {y} E}[v/x] = {fun {y} E[v/x]} ; if y =/= x
{fun {x} E}[v/x] = {fun {x} E}
eval:
eval(N) = N
eval({+ E1 E2}) = eval(E1) + eval(E2) \ if both E1 and E2
eval({- E1 E2}) = eval(E1) - eval(E2) \ evaluate to numbers
eval({* E1 E2}) = eval(E1) * eval(E2) / otherwise error!
eval({/ E1 E2}) = eval(E1) / eval(E2) /
eval(id) = error!
eval({with {x E1} E2}) = eval(E2[eval(E1)/x])
eval(FUN) = FUN ; assuming FUN is a function expression
eval({call E1 E2}) = eval(B[eval(E2)/x])
if eval(E1)={fun {x} B}, otherwise error!
|#
(define-type FLANG
[Num Number]
[Add FLANG FLANG]
[Sub FLANG FLANG]
[Mul FLANG FLANG]
[Div FLANG FLANG]
[Id Symbol]
[With Symbol FLANG FLANG]
[Fun Symbol FLANG]
[Call FLANG FLANG])
(: parse-sexpr : Sexpr -> FLANG)
;; parses s-expressions into FLANGs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])]
[(cons 'fun more)
(match sexpr
[(list 'fun (list (symbol: name)) body)
(Fun name (parse-sexpr body))]
[else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[(list 'call fun arg)
(Call (parse-sexpr fun) (parse-sexpr arg))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
(: parse : String -> FLANG)
;; parses a string containing a FLANG expression to a FLANG AST
(define (parse str)
(parse-sexpr (string->sexpr str)))
(: subst : FLANG Symbol FLANG -> FLANG)
;; substitutes the second argument with the third argument in the
;; first argument, as per the rules of substitution; the resulting
;; expression contains no free instances of the second argument
(define (subst expr from to)
(cases expr
[(Num n) expr]
[(Add l r) (Add (subst l from to) (subst r from to))]
[(Sub l r) (Sub (subst l from to) (subst r from to))]
[(Mul l r) (Mul (subst l from to) (subst r from to))]
[(Div l r) (Div (subst l from to) (subst r from to))]
[(Id name) (if (eq? name from) to expr)]
[(With bound-id named-expr bound-body)
(With bound-id
(subst named-expr from to)
(if (eq? bound-id from)
bound-body
(subst bound-body from to)))]
[(Call l r) (Call (subst l from to) (subst r from to))]
[(Fun bound-id bound-body)
(if (eq? bound-id from)
expr
(Fun bound-id (subst bound-body from to)))]))
(: Num->number : FLANG -> Number)
;; convert a FLANG number to a Racket one
(define (Num->number e)
(cases e
[(Num n) n]
[else (error 'arith-op "expected a number, got: ~s" e)]))
(: arith-op : (Number Number -> Number) FLANG FLANG -> FLANG)
;; gets a Racket numeric binary operator, and uses it within a FLANG
;; `Num' wrapper
(define (arith-op op val1 val2)
(Num (op (Num->number val1) (Num->number val2))))
(: eval : FLANG -> FLANG)
;; evaluates FLANG expressions by reducing them to *expressions* but
;; only expressions that stand for values: only `Fun`s and `Num`s
(define (eval expr)
(cases expr
[(Num n) expr]
[(Add l r) (arith-op + (eval l) (eval r))]
[(Sub l r) (arith-op - (eval l) (eval r))]
[(Mul l r) (arith-op * (eval l) (eval r))]
[(Div l r) (arith-op / (eval l) (eval r))]
[(With bound-id named-expr bound-body)
(eval (subst bound-body
bound-id
(eval named-expr)))]
[(Id name) (error 'eval "free identifier: ~s" name)]
[(Fun bound-id bound-body) expr]
[(Call fun-expr arg-expr)
(define fval (eval fun-expr))
(cases fval
[(Fun bound-id bound-body)
(eval (subst bound-body bound-id (eval arg-expr)))]
[else (error 'eval "`call' expects a function, got: ~s"
fval)])]))
(: run : String -> Number)
;; evaluate a FLANG program contained in a string
(define (run str)
(let ([result (eval (parse str))])
(cases result
[(Num n) n]
[else (error 'run "evaluation returned a non-number: ~s"
result)])))
;; tests
(test (run "{call {fun {x} {+ x 1}} 4}")
=> 5)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{call add3 1}}")
=> 4)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{with {add1 {fun {x} {+ x 1}}}
{with {x 3}
{call add1 {call add3 x}}}}}")
=> 7)
(test (run "{with {add {fun {x}
{fun {y}
{+ x y}}}}
{call {call add 8} 9}}")
=> 17)
(test (run "{with {identity {fun {x} x}}
{with {foo {fun {x} {+ x 1}}}
{call {call identity foo} 123}}}")
=> 124)
(test (run "{call {call {fun {x} {call x 1}}
{fun {x} {fun {y} {+ x y}}}}
123}")
=> 124)
Tuesday, January 28th Introducing Racket’s lambda
Quick laundry list of things to go over:
fun
&lambda
- difference between lambda and simple values
- not being able to do recursive functions with
let
- let* as a derived form
- let with lambda in Racket –> can be a derived form
- how
if
can be used to implementand
andor
as derived forms- Newtonian syntax vs. a lambda expression.
Almost all modern languages have this capability. For example, this:
(f +) ==> 5
(f *) ==> 6
(f (lambda (x y) (+ (square x) (square y)))) ==> 13
Can be written in JavaScript like this:
function square(x) { return x*x; }
console.log(f(function (x,y) { return square(x) + square(y); }));
or in ES6 JavaScript:
let square = (x) => x*x;
console.log(f((x,y) => square(x) + square(y)));
In Perl:
sub square { my ($x) = @_; return $x * $x; }
print f(sub { my ($x, $y) = @_; return square($x) + square($y); });
In Ruby:
def square(x) x*x end
puts f(lambda{|x,y| square(x) + square(y)})
etc. Even Java has lambda expressions, and “recently” C++ added them too.
Tuesday, January 28th Using Functions as Objects
A very important aspect of Racket — using “higher order” functions — functions that get and return functions. Here is a very simple example:
(define a (f 2))
(a) --> 2
(define b (f 3))
(b) --> 3
Note: what we get is actually an object that remembers (by the substitution we’re doing) a number. How about:
(aa) --> #<procedure> (this is a)
((aa)) --> 2
Take this idea to the next level:
(lambda (b)
(if b x y)))
(define (kar p) (p #t))
(define (kdr p) (p #f))
(define a (kons 1 2))
(define b (kons 3 4))
(list (kar a) (kdr a))
(list (kar b) (kdr b))
Or, with types:
(define (kons x y)
(lambda (b)
(if b x y)))
(: kar : (All (T) (Boolean -> T) -> T))
(define (kar p) (p #t))
(: kdr : (All (T) (Boolean -> T) -> T))
(define (kdr p) (p #f))
(define a (kons 1 2))
(define b (kons 3 4))
(list (kar a) (kdr a))
(list (kar b) (kdr b))
Even more — why should the internal function expect a boolean and choose what to return? We can simply expect a function that will take the two values and return one:
(define (kar p) (p (lambda (x y) x)))
(define (kdr p) (p (lambda (x y) y)))
(define a (kons 1 2))
(define b (kons 3 4))
(list (kar a) (kdr a))
(list (kar b) (kdr b))
And a typed version, using our own constructor to make it a little less painful:
(: kons : (All (A B) A B -> (Kons A B)))
(define (kons x y) (lambda (s) (s x y)))
(: kar : (All (A B) (Kons A B) -> (U A B)))
(define (kar p) (p (lambda (x y) x)))
(: kdr : (All (A B) (Kons A B) -> (U A B)))
(define (kdr p) (p (lambda (x y) y)))
(define a (kons 1 2))
(define b (kons 3 4))
(list (kar a) (kdr a))
(list (kar b) (kdr b))
Note that the Kons
type definition is the same as:
so All
is to polymorphic type definitions what lambda
is for
function definitions.
Finally, in JavaScript:
function kar(p) { return p(function(x,y){ return x; }); }
function kdr(p) { return p(function(x,y){ return y; }); }
a = kons(1,2);
b = kons(3,4);
console.log('a = <' + kar(a) + ',' + kdr(a) + '>' );
console.log('b = <' + kar(b) + ',' + kdr(b) + '>' );
or with ES6 arrow functions, the function definitions become:
const kar = p => p((x,y) => x);
const kdr = p => p((x,y) => y);
and using Typescript to add types:
const kons = <A,B>(x:A,y:B) => (s: ((x:A, y:B) => A|B)) => s(x,y);
const kar = <A,B>(p: Kons<A,B>) => p((x,y) => x);
const kdr = <A,B>(p: Kons<A,B>) => p((x,y) => y);
Tuesday, January 28th Using define-type
for new “type aliases”
As seen in these examples, there is another way to use define-type
,
using a =
to create a new type name “alias” for an existing type.
For example:
These uses of define-type
do not define any new kind of type, they are
essentially a convenience tool for making code shorter and more
readable.
(: square : NumericFunction)
(define (square n) (* n n))
Note in particular that this can also be used to define “alias type constructors” too: somewhat similar to creating new “type functions”. For example:
(: diagonal : (BinaryFun Natural Number))
(define (diagonal width height)
(sqrt (+ (* width width) (* height height))))
This is something that we will only need in a few rare cases.
Tuesday, January 28th Currying
A curried function is a function that, instead of accepting two (or more) arguments, accepts only one and returns a function that accepts the rest. For example:
(define (plus x)
(lambda (y)
(+ x y)))
It’s easy to write functions for translating between normal and curried versions.
(lambda (x)
(lambda (y)
(f x y))))
Typed version of that, with examples:
;; convert a double-argument function to a curried one
(define (currify f)
(lambda (x) (lambda (y) (f x y))))
(: add : Number Number -> Number)
(define (add x y) (+ x y))
(: plus : Number -> (Number -> Number))
(define plus (currify add))
(test ((plus 1) 2) => 3)
(test (((currify add) 1) 2) => 3)
(test (map (plus 1) '(1 2 3)) => '(2 3 4))
(test (map ((currify add) 1) '(1 2 3)) => '(2 3 4))
(test (map ((currify +) 1) '(1 2 3)) => '(2 3 4))
Usages — common with H.O. functions like map, where we want to fix one argument.
When dealing with such higher-order code, the types are very helpful, since every arrow corresponds to a function:
It is common to make the ->
function type associate to the right, so
you can find this type written as:
or even as
but that can be a little confusing…
Tuesday, January 28th Using Higher-Order & Anonymous Functions
Say that we have a function for estimating derivatives of a function at a specific point:
(: deriv : (Number -> Number) Number -> Number)
;; compute the derivative of `f' at the given point `x'
(define (deriv f x)
(/ (- (f (+ x dx)) (f x)) dx))
(: integrate : (Number -> Number) Number -> Number)
;; compute an integral of `f' at the given point `x'
(define (integrate f x)
(: loop : Number Number -> Number)
(define (loop y acc)
(if (> y x)
(* acc dx)
(loop (+ y dx) (+ acc (f y)))))
(loop 0 0))
And say that we want to try out various functions given some plot
function that draws graphs of numeric functions, for example:
The problem is that plot
expects a single (Number -> Number)
function — if we want to try it with a derivative, we can do this:
;; the derivative of sin
(define sin-deriv (lambda (x) (deriv sin x)))
(plot sin-deriv)
But this will get very tedious very fast — it is much simpler to use an anonymous function:
we can even verify that our derivative is correct by comparing a known function to its derivative
But it’s still not completely natural to do these things — you need to explicitly combine functions, which is not too convenient. Instead of doing this, we can write H.O. functions that will work with functional inputs and outputs. For example, we can write a function to subtract functions:
-> (Number -> Number))
;; subtracts two numeric 1-argument functions
(define (fsub f g)
(lambda (x) (- (f x) (g x))))
and the same for the derivative:
;; compute the derivative function of `f'
(define (fderiv f)
(lambda (x) (deriv f x)))
Now we can try the same in a much easier way:
More than that — our fderiv
could be created from deriv
automatically:
;; convert a double-argument function to a curried one
(define (currify f)
(lambda (x) (lambda (y) (f x y))))
(: fderiv : (Number -> Number) -> (Number -> Number))
;; compute the derivative function of `f'
(define fderiv (currify deriv))
Same principle with fsub
: we can write a function that converts a
binary arithmetical function into a function that operates on unary
numeric function. But to make things more readable we can define new
types for unary and binary numeric functions:
(define-type BinaryFun = (Number Number -> Number))
(: binop->fbinop : BinaryFun -> (UnaryFun UnaryFun -> UnaryFun))
;; turns an arithmetic binary operator to a function operator
(define (binop->fbinop op)
(lambda (f g)
(lambda (x) (op (f x) (g x)))))
(: fsub : UnaryFun UnaryFun -> UnaryFun)
;; functional pointwise subtraction
(define fsub (binop->fbinop -))
We can do this with anything — developing a rich library of functions and functionals (functions over functions) is extremely easy… Here’s a pretty extensive yet very short library of functions:
(define (currify f)
(lambda (x) (lambda (y) (f x y))))
(define (binop->fbinop op)
(lambda (f g)
(lambda (x) (op (f x) (g x)))))
(define (compose f g)
(lambda (x) (f (g x))))
(define dx 0.01)
(define (deriv f x)
(/ (- (f (+ x dx)) (f x)) dx))
(define (integrate f x)
(define over? (if (< x 0) < >))
(define step (if (< x 0) - +))
(define add (if (< x 0) - +))
(define (loop y acc)
(if (over? y x)
(* acc dx)
(loop (step y dx) (add acc (f y)))))
(loop 0 0))
(define fadd (binop->fbinop +))
(define fsub (binop->fbinop -))
(define fmul (binop->fbinop *))
(define fdiv (binop->fbinop /))
(define fderiv (currify deriv))
(define fintegrate (currify integrate))
;; ...
This is written in the “untyped dialect” of the class language, but it should be easy now to add the types.
Examples:
;; take a function, subtract it from its derivative's integral
(plot (fsub sin (fintegrate (fderiv sin))))
;; want to magnify the errors? -- here's how you magnify:
(plot (compose ((currify *) 5) sin))
;; so:
(plot (compose ((currify *) 20)
(fsub sin (fintegrate (fderiv sin)))))
Tuesday, January 28th Side-note: “Point-Free” combinators
Forming functions without using
lambda
(or an implicitlambda
using adefine
syntactic sugar) is called point-free style. It’s especially popular in Haskell, where it is easier to form functions this way because of implicit currying and a large number of higher level function combinators. If used too much, it can easily lead to obfuscated code.
Tuesday, January 28th This is not Runtime Code Generation
All of this is similar to run-time code generation, but not really. The
only thing that fderiv
does is take a function and store it somewhere
in the returned function, then when that function receives a number, it
uses the stored function and send it to deriv with the number. We could
simply write deriv as what fderiv
is — which is the real
derivative function:
(lambda (x)
(/ (- (f (+ x dx)) (f x)) dx)))
but again, this is not faster or slower than the plain deriv
. However,
there are some situations where we can do some of the computation on the
first-stage argument, saving work from the second stage. Here is a
cooked-to-exaggeration example — we want a function that receives two
inputs x
, y
and returns fib(x)*y
, but we must use a stupid fib
:
(if (<= n 1)
n
(+ (fib (- n 1)) (fib (- n 2)))))
The function we want is:
(* (fib x) y))
If we currify it as usual (or just use currify
), we get:
(lambda (y)
(* (fib x) y)))
And try this several times:
(map bogus36 '(1 2 3 4 5))
But in the definition of bogus
, notice that (fib x)
does not depend
on y
— so we can rewrite it a little differently:
(let ([fibx (fib x)])
(lambda (y)
(* fibx y))))
and trying the above again is much faster now:
(map bogus36 '(1 2 3 4 5))
This is therefore not doing any kind of runtime code generation, but it enables doing similar optimizations in our code. A proper RTCG facility would recompile the curried function for a given first input, and (hopefully) automatically achieve the optimization that we did in a manual way.
Tuesday, February 4th Substitution Caches
PLAI §5 (called “deferred substitutions” there)
Evaluating using substitutions is very inefficient — at each scope, we copy a piece of the program AST. This includes all function calls which implies an impractical cost (function calls should be cheap!).
To get over this, we want to use a cache of substitutions.
Basic idea: we begin evaluating with no cached substitutions, then collect them as we encounter bindings.
[Implies another change for our evaluator: we don’t really substitute identifiers until we get to them; when we reach an identifier, it is no longer an error — we must consult the substitution cache.]
Tuesday, February 4th Implementation of Cache Functionality
First, we need a type for a substitution cache. For this we will use a list of lists of two elements each — a name and its value FLANG:
(define-type SubstCache = (Listof (List Symbol FLANG)))
We need to have an empty substitution cache, a way to extend it, and a way to look things up:
(define empty-subst null)
(: extend : Symbol FLANG SubstCache -> SubstCache)
;; extend a given substitution cache with a new mapping
(define (extend id expr sc)
(cons (list id expr) sc))
(: lookup : Symbol SubstCache -> FLANG)
;; lookup a symbol in a substitution cache, return the value it is
;; bound to (or throw an error if it isn't bound)
(define (lookup name sc)
(cond [(null? sc) (error 'lookup "no binding for ~s" name)]
[(eq? name (first (first sc))) (second (first sc))]
[else (lookup name (rest sc))]))
Actually, the reason to use such list of lists is that Racket has a
built-in function called assq
that will do this kind of search (assq
is a search in an association list using eq?
for the key comparison).
This is a version of lookup
that uses assq
:
(let ([cell (assq name sc)])
(if cell
(second cell)
(error 'lookup "no binding for ~s" name))))
Tuesday, February 4th Formal Rules for Cached Substitutions
The formal evaluation rules are now different. Evaluation carries along
a substitution cache that begins its life as empty: so eval
needs an
extra argument. We begin by writing the rules that deal with the cache,
and use the above function names for simplicity — the behavior of the
three definitions can be summed up in a single rule for lookup
:
lookup(x,extend(x,E,sc)) = E
lookup(x,extend(y,E,sc)) = lookup(x,sc) if `x` is not `y`
And now we can write the new rules for eval
eval({+ E1 E2},sc) = eval(E1,sc) + eval(E2,sc)
eval({- E1 E2},sc) = eval(E1,sc) - eval(E2,sc)
eval({* E1 E2},sc) = eval(E1,sc) * eval(E2,sc)
eval({/ E1 E2},sc) = eval(E1,sc) / eval(E2,sc)
eval(x,sc) = lookup(x,sc)
eval({with {x E1} E2},sc) = eval(E2,extend(x,eval(E1,sc),sc))
eval({fun {x} E},sc) = {fun {x} E}
eval({call E1 E2},sc) = eval(B,extend(x,eval(E2,sc),sc))
if eval(E1,sc) = {fun {x} B}
= error! otherwise
Note that there is no mention of subst
— the whole point is that we
don’t really do substitution, but use the cache instead. The lookup
rules, and the places where extend
is used replaces subst
, and
therefore specifies our scoping rules.
Also note that the rule for call
is still very similar to the rule for
with
, but it looks like we have lost something — the interesting bit
with substituting into fun
expressions.
Tuesday, February 4th Evaluating with Substitution Caches
Implementing the new eval
is easy now — it is extended in the same
way that the formal eval
rule is extended:
;; evaluates FLANG expressions by reducing them to expressions
(define (eval expr sc)
(cases expr
[(Num n) expr]
[(Add l r) (arith-op + (eval l sc) (eval r sc))]
[(Sub l r) (arith-op - (eval l sc) (eval r sc))]
[(Mul l r) (arith-op * (eval l sc) (eval r sc))]
[(Div l r) (arith-op / (eval l sc) (eval r sc))]
[(With bound-id named-expr bound-body)
(eval bound-body
(extend bound-id (eval named-expr sc) sc))]
[(Id name) (lookup name sc)]
[(Fun bound-id bound-body) expr]
[(Call fun-expr arg-expr)
(define fval (eval fun-expr sc))
(cases fval
[(Fun bound-id bound-body)
(eval bound-body (extend bound-id (eval arg-expr sc) sc))]
[else (error 'eval "`call' expects a function, got: ~s"
fval)])]))
Again, note that we don’t need subst
anymore, but the rest of the code
(the data type definition, parsing, and arith-op
) is exactly the same.
Finally, we need to make sure that eval
is initially called with an
empty cache. This is easy to change in our main run
entry point:
;; evaluate a FLANG program contained in a string
(define (run str)
(let ([result (eval (parse str) empty-subst)])
(cases result
[(Num n) n]
[else (error 'run "evaluation returned a non-number: ~s"
result)])))
The full code (including the same tests, but not including formal rules for now) follows. Note that one test does not pass.
(define-type FLANG
[Num Number]
[Add FLANG FLANG]
[Sub FLANG FLANG]
[Mul FLANG FLANG]
[Div FLANG FLANG]
[Id Symbol]
[With Symbol FLANG FLANG]
[Fun Symbol FLANG]
[Call FLANG FLANG])
(: parse-sexpr : Sexpr -> FLANG)
;; parses s-expressions into FLANGs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])]
[(cons 'fun more)
(match sexpr
[(list 'fun (list (symbol: name)) body)
(Fun name (parse-sexpr body))]
[else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[(list 'call fun arg)
(Call (parse-sexpr fun) (parse-sexpr arg))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
(: parse : String -> FLANG)
;; parses a string containing a FLANG expression to a FLANG AST
(define (parse str)
(parse-sexpr (string->sexpr str)))
;; a type for substitution caches:
(define-type SubstCache = (Listof (List Symbol FLANG)))
(: empty-subst : SubstCache)
(define empty-subst null)
(: extend : Symbol FLANG SubstCache -> SubstCache)
;; extend a given substitution cache with a new mapping
(define (extend name val sc)
(cons (list name val) sc))
(: lookup : Symbol SubstCache -> FLANG)
;; lookup a symbol in a substitution cache, return the value it is
;; bound to (or throw an error if it isn't bound)
(define (lookup name sc)
(let ([cell (assq name sc)])
(if cell
(second cell)
(error 'lookup "no binding for ~s" name))))
(: Num->number : FLANG -> Number)
;; convert a FLANG number to a Racket one
(define (Num->number e)
(cases e
[(Num n) n]
[else (error 'arith-op "expected a number, got: ~s" e)]))
(: arith-op : (Number Number -> Number) FLANG FLANG -> FLANG)
;; gets a Racket numeric binary operator, and uses it within a FLANG
;; `Num' wrapper
(define (arith-op op val1 val2)
(Num (op (Num->number val1) (Num->number val2))))
(: eval : FLANG SubstCache -> FLANG)
;; evaluates FLANG expressions by reducing them to expressions
(define (eval expr sc)
(cases expr
[(Num n) expr]
[(Add l r) (arith-op + (eval l sc) (eval r sc))]
[(Sub l r) (arith-op - (eval l sc) (eval r sc))]
[(Mul l r) (arith-op * (eval l sc) (eval r sc))]
[(Div l r) (arith-op / (eval l sc) (eval r sc))]
[(With bound-id named-expr bound-body)
(eval bound-body
(extend bound-id (eval named-expr sc) sc))]
[(Id name) (lookup name sc)]
[(Fun bound-id bound-body) expr]
[(Call fun-expr arg-expr)
(define fval (eval fun-expr sc))
(cases fval
[(Fun bound-id bound-body)
(eval bound-body (extend bound-id (eval arg-expr sc) sc))]
[else (error 'eval "`call' expects a function, got: ~s"
fval)])]))
(: run : String -> Number)
;; evaluate a FLANG program contained in a string
(define (run str)
(let ([result (eval (parse str) empty-subst)])
(cases result
[(Num n) n]
[else (error 'run "evaluation returned a non-number: ~s"
result)])))
;; tests
(test (run "{call {fun {x} {+ x 1}} 4}")
=> 5)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{call add3 1}}")
=> 4)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{with {add1 {fun {x} {+ x 1}}}
{with {x 3}
{call add1 {call add3 x}}}}}")
=> 7)
(test (run "{with {identity {fun {x} x}}
{with {foo {fun {x} {+ x 1}}}
{call {call identity foo} 123}}}")
=> 124)
(test (run "{call {with {x 3}
{fun {y} {+ x y}}}
4}")
=> 7)
(test (run "{with {f {with {x 3} {fun {y} {+ x y}}}}
{with {x 100}
{call f 4}}}")
=> 7)
(test (run "{with {x 3}
{with {f {fun {y} {+ x y}}}
{with {x 5}
{call f 4}}}}")
=> "???")
(test (run "{call {call {fun {x} {call x 1}}
{fun {x} {fun {y} {+ x y}}}}
123}")
=> 124)
Tuesday, February 4th Dynamic and Lexical Scopes
This seems like it should work, and it even worked on a few examples, except for one which was hard to follow. Seems like we have a bug…
Now we get to a tricky issue that managed to be a problem for lots of language implementors, including the first version of Lisp. Lets try to run the following expression — try to figure out what it will evaluate to:
{with {f {fun {y} {+ x y}}}
{with {x 5}
{call f 4}}}}")
We expect it to return 7
(at least I do!), but we get 9
instead…
The question is — should it return 9
?
What we have arrived to is called dynamic scope. Scope is determined by the dynamic run-time environment (which is represented by our substitution cache). This is almost always undesirable, as I hope to convince you.
Before we start, we define two scope options for a programming language:
-
Static Scope (also called Lexical Scope): In a language with static scope, each identifier gets its value from the scope of its definition, not its use.
-
Dynamic Scope: In a language with dynamic scope, each identifier gets its value from the scope of its use, not its definition.
Racket uses lexical scope, our new evaluator uses dynamic, the old substitution-based evaluator was static etc.
As a side-remark, Lisp began its life as a dynamically-scoped language. The artifacts of this were (sort-of) dismissed as an implementation bug. When Scheme was introduced, it was the first Lisp dialect that used strictly lexical scoping, and Racket is obviously doing the same. (Some Lisp implementations used dynamic scope for interpreted code and lexical scope for compiled code!) In fact, Emacs Lisp is the only live dialects of Lisp that is still dynamically scoped by default. To see this, compare a version of the above code in Racket:
(let ((f (lambda (y) (+ x y))))
(let ((x 5))
(f 4))))
and the Emacs Lisp version (which looks almost the same):
(let ((f (lambda (y) (+ x y))))
(let ((x 5))
(funcall f 4))))
which also happens when we use another function on the way:
(funcall func val))
(let ((x 3))
(let ((f (lambda (y) (+ x y))))
(let ((x 5))
(blah f 4))))
and note that renaming identifiers can lead to different code — change
that val
to x
:
(funcall func x))
(let ((x 3))
(let ((f (lambda (y) (+ x y))))
(let ((x 5))
(blah f 4))))
and you get 8
because the argument name changed the x
that the
internal function sees!
Consider also this Emacs Lisp function:
x)
which has no meaning by itself (x
is unbound),
but can be given a dynamic meaning using a let
:
or a function application:
(return-x))
(foo 5)
There is also a dynamically-scoped language in the course languages:
(define x 123)
(define (getx) x)
(define (bar1 x) (getx))
(define (bar2 y) (getx))
(test (getx) => 123)
(test (let ([x 456]) (getx)) => 456)
(test (getx) => 123)
(test (bar1 999) => 999)
(test (bar2 999) => 123)
(define (foo x) (define (helper) (+ x 1)) helper)
(test ((foo 0)) => 124)
;; and *much* worse:
(define (add x y) (+ x y))
(test (let ([+ *]) (add 6 7)) => 42)
Note how bad the last example gets: you basically cannot call any function and know in advance what it will do.
There are some cases where dynamic scope can be useful in that it allows you to “remotely” customize any piece of code. A good example of where this is taken to an extreme is Emacs: originally, it was based on an ancient Lisp dialect that was still dynamically scoped, but it retained this feature even when practically all Lisp dialects moved on to having lexical scope by default. The reason for this is that the danger of dynamic scope is also a way to make a very open system where almost anything can be customized by changing it “remotely”. Here’s a concrete example for a similar kind of dynamic scope usage that makes a very hackable and open system:
(define tax% 6.25)
(define (with-tax n)
(+ n (* n (/ tax% 100))))
(with-tax 10) ; how much do we pay?
(let ([tax% 17.0]) (with-tax 10)) ; how much would we pay in Israel?
;; make that into a function
(define il-tax% 17.0)
(define (ma-over-il-saving n)
(- (let ([tax% il-tax%]) (with-tax n))
(with-tax n)))
(ma-over-il-saving 10)
;; can even control that: how much would we save if
;; the tax in israel went down one percent?
(let ([il-tax% (- il-tax% 1)]) (ma-over-il-saving 10))
;; or change both: how much savings in NH instead of MA?
(let ((tax% 0.0) (il-tax% tax%))
(ma-over-il-saving 1000))
Obviously, this power to customize everything is also the main source of problems with getting no guarantees for code. A common way to get the best of both worlds is to have controllable dynamic scope. For example, Common Lisp also has lexical scope everywhere by default, but some variables can be declared as special, which means that they are dynamically scoped. The main problem with that is that you can’t tell when a variable is special by just looking at the code that uses it, so a more popular approach is the one that is used in Racket: all bindings are always lexically scoped, but there are parameters which are a kind of dynamically scoped value containers — but they are bound to plain (lexically scoped) identifiers. Here’s the same code as above, translated to Racket with parameters:
(define tax% (make-parameter 6.5)) ; create the dynamic container
(define (with-tax n)
(+ n (* n (/ (tax%) 100)))) ; note how its value is accessed
(with-tax 10) ; how much do we pay?
(parameterize ([tax% 17.0]) (with-tax 10)) ; not a `let'
;; make that into a function
(define il-tax% (make-parameter 17.0))
(define (ma-over-il-saving n)
(- (parameterize ([tax% (il-tax%)]) (with-tax n))
(with-tax n)))
(ma-over-il-saving 10)
(parameterize ([il-tax% (- (il-tax%) 1)]) (ma-over-il-saving 10))
The main point here is that the points where a dynamically scoped value
is used are under the programmer’s control — you cannot “customize”
what -
is doing, for example. This gives us back the guarantees that
we like to have (= that code works), but of course these points are
pre-determined, unlike an environment where everything can be customized
including things that are unexpectedly useful.
As a side-note, after many decades of debating this, Emacs has finally added lexical scope in its core language, but this is still determined by a flag — a global
lexical-binding
variable.
Tuesday, February 4th Dynamic versus Lexical Scope
And back to the discussion of whether we should use dynamic or lexical scope:
-
The most important fact is that we want to view programs as executed by the normal substituting evaluator. Our original motivation was to optimize evaluation only — not to change the semantics! It follows that we want the result of this optimization to behave in the same way. All we need is to evaluate:
(run "{with {x 3}
{with {f {fun {y} {+ x y}}}
{with {x 5}
{call f 4}}}}")in the original evaluator to get convinced that
7
should be the correct result (note also that the same code, when translated into Racket, evaluates to7
).(Yet, this is a very important optimization, which without it lots of programs become too slow to be feasible, so you might claim that you’re fine with the modified semantics…)
-
It does not allow using functions as objects, for example, we have seen that we have a functional representation for pairs:
(define (kons x y)
(lambda (n)
(match n
['first x]
['second y]
[else (error ...)])))
(define my-pair (kons 1 2))If this is evaluated in a dynamically-scoped language, we do get a function as a result, but the values bound to
x
andy
are now gone! Using the substitution model we substituted these values in, but now they were only held in a cache which no has no entries for them…In the same way, currying would not work, our nice
deriv
function would not work etc etc etc. -
Makes reasoning impossible, because any piece of code behaves in a way that cannot be predicted until run-time. For example, if dynamic scoping was used in Racket, then you wouldn’t be able to know what this function is doing:
(define (foo)
x)As it is, it will cause a run-time error, but if you call it like this:
(let ([x 1])
(foo))then it will return
1
, and if you later do this:(define (bar x)
(foo))
(let ([x 1])
(bar 2))then you would get
2
!These problems can be demonstrated in Emacs Lisp too, but Racket goes one step further — it uses the same rule for evaluating a function as well as its values (Lisp uses a different name-space for functions). Because of this, you cannot even rely on the following function:
(define (add x y)
(+ x y))to always add
x
andy
! — A similar example to the above:(let ([+ -])
(add 1 2))would return
-1
! -
Many so-called “scripting” languages begin their lives with dynamic scoping. The main reason, as we’ve seen, is that implementing it is extremely simple (no, nobody does substitution in the real world! (Well, almost nobody…)).
Another reason is that these problems make life impossible if you want to use functions as object like you do in Racket, so you notice them very fast — but in a
normal
language without first-class functions, problems are not as obvious. -
For example, bash has
local
variables, but they have dynamic scope:x="the global x"
print_x() { echo "The current value of x is \"$x\""; }
foo() { local x="x from foo"; print_x; }
print_x; foo; print_xPerl began its life with dynamic scope for variables that are declared
local
:$x="the global x";
sub print_x { print "The current value of x is \"$x\"\n"; }
sub foo { local($x); $x="x from foo"; print_x; }
print_x; foo; print_x;When faced with this problem, “the Perl way” was, obviously, not to remove or fix features, but to pile them up — so
local
still behaves in this way, and now there is amy
declaration which achieves proper lexical scope (and every serious Perl programmer knows that you should always usemy
)…There are other examples of languages that changed, and languages that want to change (e.g, nobody likes dynamic scope in Emacs Lisp, but there’s just too much code now).
-
This is still a tricky issue, like any other issue with bindings. For example, googling got me quickly to a Python blog post which is confused about what “dynamic scoping” is… It claims that Python uses dynamic scope (Search for “Python uses dynamic as opposed to lexical scoping”), yet python always used lexical scope rules, as can be seen by translating their code to Racket (ignore side-effects in this computation):
(define (orange-juice)
(* x 2))
(define x 3)
(define y (orange-juice)) ; y is now 6
(define x 1)
(define y (orange-juice)) ; y is now 2or by trying this in Python:
def orange_juice():
return x*2
def foo(x):
return orange_juice()
foo(2)The real problem of python (pre 2.1, and pre 2.2 without the funny
from __future__ import nested_scopeline) is that it didn’t create closures, which we will talk about shortly.
-
Another example, which is an indicator of how easy it is to mess up your scope is the following Ruby bug — running in
irb
:% irb
irb(main):001:0> x = 0
=> 0
irb(main):002:0> lambda{|x| x}.call(5)
=> 5
irb(main):003:0> x
=> 5(This is a bug due to weird scoping rules for variables, which was fixed in newer versions of Ruby. See this Ruby rant for details, or read about Ruby and the principle of unwelcome surprise for additional gems (the latter is gone, so you’ll need the web archive to read it).)
-
Another thing to consider is the fact that compilation is something that you do based only on the lexical structure of programs, since compilers never actually run code. This means that dynamic scope makes compilation close to impossible.
-
There are some advantages for dynamic scope too. Two notable ones are:
-
Dynamic scope makes it easy to have a “configuration variable” easily change for the extent of a calling piece of code (this is used extensively in Emacs, for example). The thing is that usually we want to control which variables are “configurable” in this way, statically scoped languages like Racket often choose a separate facility for these. To rephrase the problem of dynamic scoping, it’s that all variables are modifiable.
The same can be said about functions: it is sometimes desirable to change a function dynamically (for example, see “Aspect Oriented Programming”), but if there is no control and all functions can change, we get a world where no code can every be reliable.
-
It makes recursion immediately available — for example,
{with {f {fun {x} {call f x}}}
{call f 0}}is an infinite loop with a dynamically scoped language. But in a lexically scoped language we will need to do some more work to get recursion going.
-
Tuesday, February 11th Implementing Lexical Scope: Closures and Environments
So how do we fix this?
Lets go back to the root of the problem: the new evaluator does not behave in the same way as the substituting evaluator. In the old evaluator, it was easy to see how functions can behave as objects that remember values. For example, when we do this:
{fun {y}
{+ x y}}}
the result was a function value, which actually was the syntax object for this:
Now if we call this function from someplace else like:
{with {x 2}
{call f 3}}}
it is clear what the result will be: f is bound to a function that adds
1 to its input, so in the above the later binding for x
has no effect
at all.
But with the caching evaluator, the value of
{fun {y}
{+ x y}}}
is simply:
and there is no place where we save the 1 — that’s the root of our
problem. (That’s also what makes people suspect that using lambda
in
Racket and any other functional language involves some inefficient
code-recompiling magic.) In fact, we can verify that by inspecting the
returned value, and see that it does contain a free identifier.
Clearly, we need to create an object that contains the body and the
argument list, like the function syntax object — but we don’t do any
substitution, so in addition to the body an argument name(s) we need to
remember that we still need to substitute x
by 1
. This means that
the pieces of information we need to know are:
- body: {+ x y}
- pending substitutions: [1/x]
and that last bit has the missing 1
. The resulting object is called a
closure
because it closes the function body over the substitutions
that are still pending (its environment).
So, the first change is in the value of functions which now need all
these pieces, unlike the Fun
case for the syntax object.
A second place that needs changing is the when functions are called.
When we’re done evaluating the call
arguments (the function value and
the argument value) but before we apply the function we have two
values — there is no more use for the current substitution cache at
this point: we have finished dealing with all substitutions that were
necessary over the current expression — we now continue with
evaluating the body of the function, with the new substitutions for the
formal arguments and actual values given. But the body itself is the
same one we had before — which is the previous body with its suspended
substitutions that we still did not do.
Rewrite the evaluation rules — all are the same except for evaluating
a fun
form and a call
form:
eval({+ E1 E2},sc) = eval(E1,sc) + eval(E2,sc)
eval({- E1 E2},sc) = eval(E1,sc) - eval(E2,sc)
eval({* E1 E2},sc) = eval(E1,sc) * eval(E2,sc)
eval({/ E1 E2},sc) = eval(E1,sc) / eval(E2,sc)
eval(x,sc) = lookup(x,sc)
eval({with {x E1} E2},sc) = eval(E2,extend(x,eval(E1,sc),sc))
eval({fun {x} E},sc) = <{fun {x} E}, sc>
eval({call E1 E2},sc1) = eval(B,extend(x,eval(E2,sc1),sc2))
if eval(E1,sc1) = <{fun {x} B}, sc2>
= error! otherwise
As a side note, these substitution caches are a little more than “just a cache” now — they actually hold an environment of substitutions in which expression should be evaluated. So we will switch to the common environment name now.:
eval({+ E1 E2},env) = eval(E1,env) + eval(E2,env)
eval({- E1 E2},env) = eval(E1,env) - eval(E2,env)
eval({* E1 E2},env) = eval(E1,env) * eval(E2,env)
eval({/ E1 E2},env) = eval(E1,env) / eval(E2,env)
eval(x,env) = lookup(x,env)
eval({with {x E1} E2},env) = eval(E2,extend(x,eval(E1,env),env))
eval({fun {x} E},env) = <{fun {x} E}, env>
eval({call E1 E2},env1) = eval(B,extend(x,eval(E2, env1),env2))
if eval(E1,env1) = <{fun {x} B}, env2>
= error! otherwise
In case you find this easier to follow, the “flat algorithm” for
evaluating a call
is:
2. if f is not a <{fun ...},...> closure then error!
3. x := evaluate E2 in env1
4. new_env := extend env_of(f) by mapping arg_of(f) to x
5. evaluate (and return) body_of(f) in new_env
Note how the scoping rules that are implied by this definition match the scoping rules that were implied by the substitution-based rules. (It should be possible to prove that they are the same.)
The changes to the code are almost trivial, except that we need a way to
represent <{fun {x} B}, env>
pairs.
The implication of this change is that we now cannot use the same type for function syntax and function values since function values have more than just syntax. There is a simple solution to this — we never do any substitutions now, so we don’t need to translate values into expressions — we can come up with a new type for values, separate from the type of abstract syntax trees.
When we do this, we will also fix our hack of using FLANG as the type of
values: this was merely a convenience since the AST type had cases for
all kinds of values that we needed. (In fact, you should have noticed
that Racket does this too: numbers, strings, booleans, etc are all used
by both programs and syntax representation (s-expressions) — but note
that function values are not used in syntax.) We will now implement a
separate VAL
type for runtime values.
First, we need now a type for such environments — we can use Listof
for this:
(define-type ENV = (Listof (List Symbol VAL)))
but we can just as well define a new type for environment values:
[EmptyEnv]
[Extend Symbol VAL ENV])
Reimplementing lookup
is now simple:
;; lookup a symbol in an environment, return its value or throw an
;; error if it isn't bound
(define (lookup name env)
(cases env
[(EmptyEnv) (error 'lookup "no binding for ~s" name)]
[(Extend id val rest-env)
(if (eq? id name) val (lookup name rest-env))]))
… we don’t need extend
because we get Extend
from the type
definition, and we also get (EmptyEnv)
instead of empty-subst
.
We now use this with the new type for values — two variants of these:
[NumV Number]
[FunV Symbol FLANG ENV]) ; arg-name, body, scope
And now the new implementation of eval
which uses the new type and
implements lexical scope:
;; evaluates FLANG expressions by reducing them to values
(define (eval expr env)
(cases expr
[(Num n) (NumV n)]
[(Add l r) (arith-op + (eval l env) (eval r env))]
[(Sub l r) (arith-op - (eval l env) (eval r env))]
[(Mul l r) (arith-op * (eval l env) (eval r env))]
[(Div l r) (arith-op / (eval l env) (eval r env))]
[(With bound-id named-expr bound-body)
(eval bound-body
(Extend bound-id (eval named-expr env) env))]
[(Id name) (lookup name env)]
[(Fun bound-id bound-body)
(FunV bound-id bound-body env)]
[(Call fun-expr arg-expr)
(define fval (eval fun-expr env))
(cases fval
[(FunV bound-id bound-body f-env)
(eval bound-body
(Extend bound-id (eval arg-expr env) f-env))]
[else (error 'eval "`call' expects a function, got: ~s"
fval)])]))
We also need to update arith-op
to use VAL
objects. The full code
follows — it now passes all tests, including the example that we used
to find the problem.
flang-env.rkt D ;; The Flang interpreter, using environments
#lang pl
#|
The grammar:
<FLANG> ::= <num>
| { + <FLANG> <FLANG> }
| { - <FLANG> <FLANG> }
| { * <FLANG> <FLANG> }
| { / <FLANG> <FLANG> }
| { with { <id> <FLANG> } <FLANG> }
| <id>
| { fun { <id> } <FLANG> }
| { call <FLANG> <FLANG> }
Evaluation rules:
eval(N,env) = N
eval({+ E1 E2},env) = eval(E1,env) + eval(E2,env)
eval({- E1 E2},env) = eval(E1,env) - eval(E2,env)
eval({* E1 E2},env) = eval(E1,env) * eval(E2,env)
eval({/ E1 E2},env) = eval(E1,env) / eval(E2,env)
eval(x,env) = lookup(x,env)
eval({with {x E1} E2},env) = eval(E2,extend(x,eval(E1,env),env))
eval({fun {x} E},env) = <{fun {x} E}, env>
eval({call E1 E2},env1) = eval(B,extend(x,eval(E2,env1),env2))
if eval(E1,env1) = <{fun {x} B}, env2>
= error! otherwise
|#
(define-type FLANG
[Num Number]
[Add FLANG FLANG]
[Sub FLANG FLANG]
[Mul FLANG FLANG]
[Div FLANG FLANG]
[Id Symbol]
[With Symbol FLANG FLANG]
[Fun Symbol FLANG]
[Call FLANG FLANG])
(: parse-sexpr : Sexpr -> FLANG)
;; parses s-expressions into FLANGs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])]
[(cons 'fun more)
(match sexpr
[(list 'fun (list (symbol: name)) body)
(Fun name (parse-sexpr body))]
[else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[(list 'call fun arg)
(Call (parse-sexpr fun) (parse-sexpr arg))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
(: parse : String -> FLANG)
;; parses a string containing a FLANG expression to a FLANG AST
(define (parse str)
(parse-sexpr (string->sexpr str)))
;; Types for environments, values, and a lookup function
(define-type ENV
[EmptyEnv]
[Extend Symbol VAL ENV])
(define-type VAL
[NumV Number]
[FunV Symbol FLANG ENV])
(: lookup : Symbol ENV -> VAL)
;; lookup a symbol in an environment, return its value or throw an
;; error if it isn't bound
(define (lookup name env)
(cases env
[(EmptyEnv) (error 'lookup "no binding for ~s" name)]
[(Extend id val rest-env)
(if (eq? id name) val (lookup name rest-env))]))
(: NumV->number : VAL -> Number)
;; convert a FLANG runtime numeric value to a Racket one
(define (NumV->number val)
(cases val
[(NumV n) n]
[else (error 'arith-op "expected a number, got: ~s" val)]))
(: arith-op : (Number Number -> Number) VAL VAL -> VAL)
;; gets a Racket numeric binary operator, and uses it within a NumV
;; wrapper
(define (arith-op op val1 val2)
(NumV (op (NumV->number val1) (NumV->number val2))))
(: eval : FLANG ENV -> VAL)
;; evaluates FLANG expressions by reducing them to values
(define (eval expr env)
(cases expr
[(Num n) (NumV n)]
[(Add l r) (arith-op + (eval l env) (eval r env))]
[(Sub l r) (arith-op - (eval l env) (eval r env))]
[(Mul l r) (arith-op * (eval l env) (eval r env))]
[(Div l r) (arith-op / (eval l env) (eval r env))]
[(With bound-id named-expr bound-body)
(eval bound-body
(Extend bound-id (eval named-expr env) env))]
[(Id name) (lookup name env)]
[(Fun bound-id bound-body)
(FunV bound-id bound-body env)]
[(Call fun-expr arg-expr)
(define fval (eval fun-expr env))
(cases fval
[(FunV bound-id bound-body f-env)
(eval bound-body
(Extend bound-id (eval arg-expr env) f-env))]
[else (error 'eval "`call' expects a function, got: ~s"
fval)])]))
(: run : String -> Number)
;; evaluate a FLANG program contained in a string
(define (run str)
(let ([result (eval (parse str) (EmptyEnv))])
(cases result
[(NumV n) n]
[else (error 'run "evaluation returned a non-number: ~s"
result)])))
;; tests
(test (run "{call {fun {x} {+ x 1}} 4}")
=> 5)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{call add3 1}}")
=> 4)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{with {add1 {fun {x} {+ x 1}}}
{with {x 3}
{call add1 {call add3 x}}}}}")
=> 7)
(test (run "{with {identity {fun {x} x}}
{with {foo {fun {x} {+ x 1}}}
{call {call identity foo} 123}}}")
=> 124)
(test (run "{with {x 3}
{with {f {fun {y} {+ x y}}}
{with {x 5}
{call f 4}}}}")
=> 7)
(test (run "{call {with {x 3}
{fun {y} {+ x y}}}
4}")
=> 7)
(test (run "{with {f {with {x 3} {fun {y} {+ x y}}}}
{with {x 100}
{call f 4}}}")
=> 7)
(test (run "{call {call {fun {x} {call x 1}}
{fun {x} {fun {y} {+ x y}}}}
123}")
=> 124)
Tuesday, February 11th Fixing an Overlooked Bug
Incidentally, this version fixes a bug we had previously in the substitution version of FLANG:
{with {x 7}
{call f 1}}}")
This bug was due to our naive subst
, which doesn’t avoid capturing
renames. But note that since that version of the evaluator makes its way
from the outside in, there is no difference in semantics for valid
programs — ones that don’t have free identifiers.
(Reminder: This was not a dynamically scoped language, just a bug that
happened when x
wasn’t substituted away before f
was replaced with
something that refers to x
.)
Tuesday, February 11th Lexical Scope using Racket Closures
PLAI §11 (without the last part about recursion)
An alternative representation for an environment.
We’ve already seen how first-class functions can be used to implement
“objects” that contain some information. We can use the same idea to
represent an environment. The basic intuition is — an environment is a
mapping (a function) between an identifier and some value. For
example, we can represent the environment that maps 'a
to 1
and 'b
to 2
(using just numbers for simplicity) using this function:
(define (my-map id)
(cond [(eq? 'a id) 1]
[(eq? 'b id) 2]
[else (error ...)]))
An empty mapping that is implemented in this way has the same type:
(define (empty-mapping id)
(error ...))
We can use this idea to implement our environments: we only need to
define three things — EmptyEnv
, Extend
, and lookup
. If we manage
to keep the contract to these functions intact, we will be able to
simply plug it into the same evaluator code with no other changes. It
will also be more convenient to define ENV
as the appropriate function
type for use in the VAL
type definition instead of using the actual
type:
(define-type ENV = Symbol -> VAL)
Now we get to EmptyEnv
— this is expected to be a function that
expects no arguments and creates an empty environment, one that behaves
like the empty-mapping
function defined above. We could define it like
this (changing the empty-mapping
type to return a VAL
):
but we can skip the need for an extra definition and simply return an empty mapping function:
(define (EmptyEnv)
(lambda (id) (error ...)))
(The un-Rackety name is to avoid replacing previous code that used the
EmptyEnv
name for the constructor that was created by the type
definition.)
The next thing we tackle is lookup
. The previous definition that was
used is:
(define (lookup name env)
(cases env
[(EmptyEnv) (error 'lookup "no binding for ~s" name)]
[(Extend id val rest-env)
(if (eq? id name) val (lookup name rest-env))]))
How should it be modified now? Easy — an environment is a mapping: a
Racket function that will do the searching job itself. We don’t need to
modify the contract since we’re still using ENV
, except a different
implementation for it. The new definition is:
(define (lookup name env)
(env name))
Note that lookup
does almost nothing — it simply delegates the real
work to the env
argument. This is a good hint for the error message
that empty mappings should throw —
(define (EmptyEnv)
(lambda (id) (error 'lookup "no binding for ~s" id)))
Finally, Extend
— this was previously created by the variant case of
the ENV type definition:
keeping the same type that is implied by this variant means that the new
Extend
should look like this:
(define (Extend id val rest-env)
...)
The question is — how do we extend a given environment? Well, first,
we know that the result should be mapping — a symbol -> VAL
function
that expects an identifier to look for:
(define (Extend id val rest-env)
(lambda (name)
...))
Next, we know that in the generated mapping, if we look for id
then
the result should be val
:
(define (Extend id val rest-env)
(lambda (name)
(if (eq? name id)
val
...)))
If the name
that we’re looking for is not the same as id
, then we
need to search through the previous environment:
(define (Extend id val rest-env)
(lambda (name)
(if (eq? name id)
val
(lookup name rest-env))))
But we know what lookup
does — it simply delegates back to the
mapping function (which is our rest
argument), so we can take a direct
route instead:
(define (Extend id val rest-env)
(lambda (name)
(if (eq? name id)
val
(rest-env name)))) ; same as (lookup name rest-env)
To see how all this works, try out extending an empty environment a few times and examine the result. For example, the environment that we began with:
(cond [(eq? 'a id) 1]
[(eq? 'b id) 2]
[else (error ...)]))
behaves in the same way (if the type of values is numbers) as
The new code is now the same, except for the environment code:
#|
The grammar:
<FLANG> ::= <num>
| { + <FLANG> <FLANG> }
| { - <FLANG> <FLANG> }
| { * <FLANG> <FLANG> }
| { / <FLANG> <FLANG> }
| { with { <id> <FLANG> } <FLANG> }
| <id>
| { fun { <id> } <FLANG> }
| { call <FLANG> <FLANG> }
Evaluation rules:
eval(N,env) = N
eval({+ E1 E2},env) = eval(E1,env) + eval(E2,env)
eval({- E1 E2},env) = eval(E1,env) - eval(E2,env)
eval({* E1 E2},env) = eval(E1,env) * eval(E2,env)
eval({/ E1 E2},env) = eval(E1,env) / eval(E2,env)
eval(x,env) = lookup(x,env)
eval({with {x E1} E2},env) = eval(E2,extend(x,eval(E1,env),env))
eval({fun {x} E},env) = <{fun {x} E}, env>
eval({call E1 E2},env1) = eval(B,extend(x,eval(E2,env1),env2))
if eval(E1,env1) = <{fun {x} B}, env2>
= error! otherwise
|#
(define-type FLANG
[Num Number]
[Add FLANG FLANG]
[Sub FLANG FLANG]
[Mul FLANG FLANG]
[Div FLANG FLANG]
[Id Symbol]
[With Symbol FLANG FLANG]
[Fun Symbol FLANG]
[Call FLANG FLANG])
(: parse-sexpr : Sexpr -> FLANG)
;; parses s-expressions into FLANGs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])]
[(cons 'fun more)
(match sexpr
[(list 'fun (list (symbol: name)) body)
(Fun name (parse-sexpr body))]
[else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[(list 'call fun arg)
(Call (parse-sexpr fun) (parse-sexpr arg))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
(: parse : String -> FLANG)
;; parses a string containing a FLANG expression to a FLANG AST
(define (parse str)
(parse-sexpr (string->sexpr str)))
;; Types for environments, values, and a lookup function
(define-type VAL
[NumV Number]
[FunV Symbol FLANG ENV])
;; Define a type for functional environments
(define-type ENV = Symbol -> VAL)
(: EmptyEnv : -> ENV)
(define (EmptyEnv)
(lambda (id) (error 'lookup "no binding for ~s" id)))
(: Extend : Symbol VAL ENV -> ENV)
;; extend a given environment cache with a new binding
(define (Extend id val rest-env)
(lambda (name)
(if (eq? name id)
val
(rest-env name))))
(: lookup : Symbol ENV -> VAL)
;; lookup a symbol in an environment, return its value or throw an
;; error if it isn't bound
(define (lookup name env)
(env name))
(: NumV->number : VAL -> Number)
;; convert a FLANG runtime numeric value to a Racket one
(define (NumV->number val)
(cases val
[(NumV n) n]
[else (error 'arith-op "expected a number, got: ~s" val)]))
(: arith-op : (Number Number -> Number) VAL VAL -> VAL)
;; gets a Racket numeric binary operator, and uses it within a NumV
;; wrapper
(define (arith-op op val1 val2)
(NumV (op (NumV->number val1) (NumV->number val2))))
(: eval : FLANG ENV -> VAL)
;; evaluates FLANG expressions by reducing them to values
(define (eval expr env)
(cases expr
[(Num n) (NumV n)]
[(Add l r) (arith-op + (eval l env) (eval r env))]
[(Sub l r) (arith-op - (eval l env) (eval r env))]
[(Mul l r) (arith-op * (eval l env) (eval r env))]
[(Div l r) (arith-op / (eval l env) (eval r env))]
[(With bound-id named-expr bound-body)
(eval bound-body
(Extend bound-id (eval named-expr env) env))]
[(Id name) (lookup name env)]
[(Fun bound-id bound-body)
(FunV bound-id bound-body env)]
[(Call fun-expr arg-expr)
(define fval (eval fun-expr env))
(cases fval
[(FunV bound-id bound-body f-env)
(eval bound-body
(Extend bound-id (eval arg-expr env) f-env))]
[else (error 'eval "`call' expects a function, got: ~s"
fval)])]))
(: run : String -> Number)
;; evaluate a FLANG program contained in a string
(define (run str)
(let ([result (eval (parse str) (EmptyEnv))])
(cases result
[(NumV n) n]
[else (error 'run "evaluation returned a non-number: ~s"
result)])))
;; tests
(test (run "{call {fun {x} {+ x 1}} 4}")
=> 5)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{call add3 1}}")
=> 4)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{with {add1 {fun {x} {+ x 1}}}
{with {x 3}
{call add1 {call add3 x}}}}}")
=> 7)
(test (run "{with {identity {fun {x} x}}
{with {foo {fun {x} {+ x 1}}}
{call {call identity foo} 123}}}")
=> 124)
(test (run "{with {x 3}
{with {f {fun {y} {+ x y}}}
{with {x 5}
{call f 4}}}}")
=> 7)
(test (run "{call {with {x 3}
{fun {y} {+ x y}}}
4}")
=> 7)
(test (run "{with {f {with {x 3} {fun {y} {+ x y}}}}
{with {x 100}
{call f 4}}}")
=> 7)
(test (run "{call {call {fun {x} {call x 1}}
{fun {x} {fun {y} {+ x y}}}}
123}")
=> 124)
Tuesday, February 11th More Closures (on both levels)
Racket closures (= functions) can be used in other places too, and as we have seen, they can do more than encapsulate various values — they can also hold the behavior that is expected of these values.
To demonstrate this we will deal with closures in our language. We currently use a variant that holds the three pieces of relevant information:
We can replace this by a functional object, which will hold the three
values. First, change the VAL
type to hold functions for FunV
values:
[NumV Number]
[FunV (? -> ?)])
And note that the function should somehow encapsulate the same
information that was there previously, the question is how this
information is going to be done, and this will determine the actual
type. This information plays a role in two places in our evaluator —
generating a closure in the Fun
case, and using it in the Call
case:
(FunV bound-id bound-body env)]
[(Call fun-expr arg-expr)
(define fval (eval fun-expr env))
(cases fval
[(FunV bound-id bound-body f-env)
(eval bound-body ;***
(Extend bound-id ;***
(eval arg-expr env) ;***
f-env))] ;***
[else (error 'eval "`call' expects a function, got: ~s" fval)])]
we can simply fold the marked functionality bit of Call
into a Racket
function that will be stored in a FunV
object — this piece of
functionality takes an argument value, extends the closure’s environment
with its value and the function’s name, and continues to evaluate the
function body. Folding all of this into a function gives us:
(eval bound-body (Extend bound-id arg-val env)))
where the values of bound-body
, bound-id
, and val
are known at the
time that the FunV
is constructed. Doing this gives us the following
code for the two cases:
(FunV (lambda (arg-val)
(eval bound-body (Extend bound-id arg-val env))))]
[(Call fun-expr arg-expr)
(define fval (eval fun-expr env))
(cases fval
[(FunV proc) (proc (eval arg-expr env))]
[else (error 'eval "`call' expects a function, got: ~s" fval)])]
And now the type of the function is clear:
[NumV Number]
[FunV (VAL -> VAL)])
And again, the rest of the code is unmodified:
(define-type FLANG
[Num Number]
[Add FLANG FLANG]
[Sub FLANG FLANG]
[Mul FLANG FLANG]
[Div FLANG FLANG]
[Id Symbol]
[With Symbol FLANG FLANG]
[Fun Symbol FLANG]
[Call FLANG FLANG])
(: parse-sexpr : Sexpr -> FLANG)
;; parses s-expressions into FLANGs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])]
[(cons 'fun more)
(match sexpr
[(list 'fun (list (symbol: name)) body)
(Fun name (parse-sexpr body))]
[else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[(list 'call fun arg)
(Call (parse-sexpr fun) (parse-sexpr arg))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
(: parse : String -> FLANG)
;; parses a string containing a FLANG expression to a FLANG AST
(define (parse str)
(parse-sexpr (string->sexpr str)))
;; Types for environments, values, and a lookup function
(define-type VAL
[NumV Number]
[FunV (VAL -> VAL)])
;; Define a type for functional environments
(define-type ENV = Symbol -> VAL)
(: EmptyEnv : -> ENV)
(define (EmptyEnv)
(lambda (id) (error 'lookup "no binding for ~s" id)))
(: Extend : Symbol VAL ENV -> ENV)
;; extend a given environment cache with a new binding
(define (Extend id val rest-env)
(lambda (name)
(if (eq? name id)
val
(rest-env name))))
(: lookup : Symbol ENV -> VAL)
;; lookup a symbol in an environment, return its value or throw an
;; error if it isn't bound
(define (lookup name env)
(env name))
(: NumV->number : VAL -> Number)
;; convert a FLANG runtime numeric value to a Racket one
(define (NumV->number val)
(cases val
[(NumV n) n]
[else (error 'arith-op "expected a number, got: ~s" val)]))
(: arith-op : (Number Number -> Number) VAL VAL -> VAL)
;; gets a Racket numeric binary operator, and uses it within a NumV
;; wrapper
(define (arith-op op val1 val2)
(NumV (op (NumV->number val1) (NumV->number val2))))
(: eval : FLANG ENV -> VAL)
;; evaluates FLANG expressions by reducing them to values
(define (eval expr env)
(cases expr
[(Num n) (NumV n)]
[(Add l r) (arith-op + (eval l env) (eval r env))]
[(Sub l r) (arith-op - (eval l env) (eval r env))]
[(Mul l r) (arith-op * (eval l env) (eval r env))]
[(Div l r) (arith-op / (eval l env) (eval r env))]
[(With bound-id named-expr bound-body)
(eval bound-body
(Extend bound-id (eval named-expr env) env))]
[(Id name) (lookup name env)]
[(Fun bound-id bound-body)
(FunV (lambda (arg-val)
(eval bound-body (Extend bound-id arg-val env))))]
[(Call fun-expr arg-expr)
(define fval (eval fun-expr env))
(cases fval
[(FunV proc) (proc (eval arg-expr env))]
[else (error 'eval "`call' expects a function, got: ~s"
fval)])]))
(: run : String -> Number)
;; evaluate a FLANG program contained in a string
(define (run str)
(let ([result (eval (parse str) (EmptyEnv))])
(cases result
[(NumV n) n]
[else (error 'run "evaluation returned a non-number: ~s"
result)])))
;; tests
(test (run "{call {fun {x} {+ x 1}} 4}")
=> 5)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{call add3 1}}")
=> 4)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{with {add1 {fun {x} {+ x 1}}}
{with {x 3}
{call add1 {call add3 x}}}}}")
=> 7)
(test (run "{with {identity {fun {x} x}}
{with {foo {fun {x} {+ x 1}}}
{call {call identity foo} 123}}}")
=> 124)
(test (run "{with {x 3}
{with {f {fun {y} {+ x y}}}
{with {x 5}
{call f 4}}}}")
=> 7)
(test (run "{call {with {x 3}
{fun {y} {+ x y}}}
4}")
=> 7)
(test (run "{with {f {with {x 3} {fun {y} {+ x y}}}}
{with {x 100}
{call f 4}}}")
=> 7)
(test (run "{call {call {fun {x} {call x 1}}
{fun {x} {fun {y} {+ x y}}}}
123}")
=> 124)
Tuesday, February 11th Types of Evaluators
What we did just now is implement lexical environments and closures in the language we implement using lexical environments and closures in our own language (Racket)!
This is another example of embedding a feature of the host language in the implemented language, an issue that we have already discussed.
There are many examples of this, even when the two languages involved are different. For example, if we have this bit in the C implementation of Racket:
Racket_Object *eval_and(int argc, Racket_Object *argv[]) {
Racket_Object *tmp;
if ( argc != 2 )
signal_racket_error("bad number of arguments");
else if ( racket_eval(argv[0]) != racket_false
&&
(tmp = racket_eval(argv[1])) != racket_false )
return tmp;
else
return racket_false;
}
then the special semantics of evaluating a Racket and
form is being
inherited from C’s special treatment of &&
. You can see this by the
fact that if there is a bug in the C compiler, then it will propagate to
the resulting Racket implementation too. A different solution is to not
use &&
at all:
Racket_Object *eval_and(int argc, Racket_Object *argv[]) {
Racket_Object *tmp;
if ( argc != 2 )
signal_racket_error("bad number of arguments");
else if ( racket_eval(argv[0]) != racket_false )
return racket_eval(argv[1]);
else
return racket_false;
}
and we can say that this is even better since it evaluates the second expression in tail position. But in this case we don’t really get that benefit, since C itself is not doing tail-call optimization as a standard feature (though some compilers do so under some circumstances).
We have seen a few different implementations of evaluators that are quite different in flavor. They suggest the following taxonomy.
-
A syntactic evaluator is one that uses its own language to represent expressions and semantic runtime values of the evaluated language, implementing all the corresponding behavior explicitly.
-
A meta evaluator is an evaluator that uses language features of its own language to directly implement behavior of the evaluated language.
While our substitution-based FLANG evaluator was close to being a syntactic evaluator, we haven’t written any purely syntactic evaluators so far: we still relied on things like Racket arithmetics etc. The most recent evaluator that we have studied, is even more of a meta evaluator than the preceding ones: it doesn’t even implement closures and lexical scope, and instead, it uses the fact that Racket itself has them.
With a good match between the evaluated language and the implementation language, writing a meta evaluator can be very easy. With a bad match, though, it can be very hard. With a syntactic evaluator, implementing each semantic feature will be somewhat hard, but in return you don’t have to worry as much about how well the implementation and the evaluated languages match up. In particular, if there is a particularly strong mismatch between the implementation and the evaluated language, it may take less effort to write a syntactic evaluator than a meta evaluator.
As an exercise, we can build upon our latest evaluator to remove the encapsulation of the evaluator’s response in the VAL type. The resulting evaluator is shown below. This is a true meta evaluator: it uses Racket closures to implement FLANG closures, Racket function application for FLANG function application, Racket numbers for FLANG numbers, and Racket arithmetic for FLANG arithmetic. In fact, ignoring some small syntactic differences between Racket and FLANG, this latest evaluator can be classified as something more specific than a meta evaluator:
-
A meta-circular evaluator is a meta evaluator in which the implementation and the evaluated languages are the same.
This is essentially the concept of a “universal” evaluator, as in a “universal turing machine”.
(Put differently, the trivial nature of the evaluator clues us in to the deep connection between the two languages, whatever their syntactic differences may be.)
Tuesday, February 11th Feature Embedding
We saw that the difference between lazy evaluation and eager evaluation
is in the evaluation rules for with
forms, function applications, etc:
is eager, and
is lazy. But is the first rule really eager? The fact is that the only thing that makes it eager is the fact that our understanding of the mathematical notation is eager — if we were to take math as lazy, then the description of the rule becomes a description of lazy evaluation.
Another way to look at this is — take the piece of code that implements this evaluation:
;; evaluates FLANG expressions by reducing them to numbers
(define (eval expr)
(cases expr
...
[(With bound-id named-expr bound-body)
(eval (subst bound-body
bound-id
(Num (eval named-expr))))]
...))
and the same question applies: is this really implementing eager
evaluation? We know that this is indeed eager — we can simply try it
and check that it is, but it is only eager because we are using an eager
language for the implementation! If our own language was lazy, then the
evaluator’s implementation would run lazily, which means that the above
applications of the eval
and the subst
functions would also be lazy,
making our evaluator lazy as well.
This is a general phenomena where some of the semantic features of the language we use (math in the formal description, Racket in our code) gets embedded into the language we implement.
Here’s another example — consider the code that implements arithmetics:
;; evaluates FLANG expressions by reducing them to numbers
(define (eval expr)
(cases expr
[(Num n) n]
[(Add l r) (+ (eval l) (eval r))]
...))
what if it was written like this:
if (is_Num(expr))
return num_of_Num(expr);
else if (is_Add(expr))
return eval(lhs_of_Add(expr)) + eval(rhs_of_Add(expr));
else if ...
...
}
Would it still implement unlimited integers and exact fractions? That depends on the language that was used to implement it: the above syntax suggests C, C++, Java, or some other relative, which usually come with limited integers and no exact fractions. But this really depends on the language — even our own code has unlimited integers and exact rationals only because Racket has them. If we were using a language that didn’t have such features (there are such Scheme implementations), then our implemented language would absorb these (lack of) features too, and its own numbers would be limited in just the same way. (And this includes the syntax for numbers, which we embedded intentionally, like the syntax for identifiers).
The bottom line is that we should be aware of such issues, and be very careful when we talk about semantics. Even the language that we use to communicate (semi-formal logic) can mean different things.
Aside: read “Reflections on Trusting Trust” by Ken Thompson (You can skip to the “Stage II” part to get to the interesting stuff.)
(And when you’re done, look for “XcodeGhost” to see a relevant example, and don’t miss the leaked document on the wikipedia page…)
Here is yet another variation of our evaluator that is even closer to a meta-circular evaluator. It uses Racket values directly to implement values, so arithmetic operations become straightforward. Note especially how the case for function application is similar to arithmetics: a FLANG function application translates to a Racket function application. In both cases (applications and arithmetics) we don’t even check the objects since they are simple Racket objects — if our language happens to have some meaning for arithmetics with functions, or for applying numbers, then we will inherit the same semantics in our language. This means that we now specify less behavior and fall back more often on what Racket does.
We use Racket values with this type definition:
And the evaluation function can now be:
;; evaluates FLANG expressions by reducing them to values
(define (eval expr env)
(cases expr
[(Num n) n] ;*** return the actual number
[(Add l r) (+ (eval l env) (eval r env))]
[(Sub l r) (- (eval l env) (eval r env))]
[(Mul l r) (* (eval l env) (eval r env))]
[(Div l r) (/ (eval l env) (eval r env))]
[(With bound-id named-expr bound-body)
(eval bound-body
(Extend bound-id (eval named-expr env) env))]
[(Id name) (lookup name env)]
[(Fun bound-id bound-body)
(lambda ([arg-val : VAL]) ;*** return the racket function
;; note that this requires input type specifications since
;; typed racket can't guess the right one
(eval bound-body (Extend bound-id arg-val env)))]
[(Call fun-expr arg-expr)
((eval fun-expr env) ;*** trivial like the arithmetics!
(eval arg-expr env))]))
Note how the arithmetics implementation is simple — it’s a direct translation of the FLANG syntax to Racket operations, and since we don’t check the inputs to the Racket operations, we let Racket throw type errors for us. Note also how function application is just like the arithmetic operations: a FLANG application is directly translated to a Racket application.
However, this does not work quite as simply in Typed Racket. The whole
point of typechecking is that we never run into type errors — so we
cannot throw back on Racket errors since code that might produce them is
forbidden! A way around this is to perform explicit checks that
guarantee that Racket cannot run into type errors. We do this with the
following two helpers that are defined inside eval
:
(define (evalN e)
(let ([n (eval e env)])
(if (number? n)
n
(error 'eval "got a non-number: ~s" n))))
(: evalF : FLANG -> (VAL -> VAL))
(define (evalF e)
(let ([f (eval e env)])
(if (function? f)
f
(error 'eval "got a non-function: ~s" f))))
Note that Typed Racket is “smart enough” to figure out that in evalF
the result of the recursive evaluation has to be either Number
or
(VAL -> VAL)
; and since the if
throws out on numbers, we’re left
with (VAL -> VAL)
functions, not just any function.
(define-type FLANG
[Num Number]
[Add FLANG FLANG]
[Sub FLANG FLANG]
[Mul FLANG FLANG]
[Div FLANG FLANG]
[Id Symbol]
[With Symbol FLANG FLANG]
[Fun Symbol FLANG]
[Call FLANG FLANG])
(: parse-sexpr : Sexpr -> FLANG)
;; parses s-expressions into FLANGs
(define (parse-sexpr sexpr)
(match sexpr
[(number: n) (Num n)]
[(symbol: name) (Id name)]
[(cons 'with more)
(match sexpr
[(list 'with (list (symbol: name) named) body)
(With name (parse-sexpr named) (parse-sexpr body))]
[else (error 'parse-sexpr "bad `with' syntax in ~s" sexpr)])]
[(cons 'fun more)
(match sexpr
[(list 'fun (list (symbol: name)) body)
(Fun name (parse-sexpr body))]
[else (error 'parse-sexpr "bad `fun' syntax in ~s" sexpr)])]
[(list '+ lhs rhs) (Add (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '- lhs rhs) (Sub (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '* lhs rhs) (Mul (parse-sexpr lhs) (parse-sexpr rhs))]
[(list '/ lhs rhs) (Div (parse-sexpr lhs) (parse-sexpr rhs))]
[(list 'call fun arg)
(Call (parse-sexpr fun) (parse-sexpr arg))]
[else (error 'parse-sexpr "bad syntax in ~s" sexpr)]))
(: parse : String -> FLANG)
;; parses a string containing a FLANG expression to a FLANG AST
(define (parse str)
(parse-sexpr (string->sexpr str)))
;; Types for environments, values, and a lookup function
;; Values are plain Racket values, no new VAL wrapper;
;; (but note that this is a recursive definition)
(define-type VAL = (U Number (VAL -> VAL)))
;; Define a type for functional environments
(define-type ENV = (Symbol -> VAL))
(: EmptyEnv : -> ENV)
(define (EmptyEnv)
(lambda (id) (error 'lookup "no binding for ~s" id)))
(: Extend : Symbol VAL ENV -> ENV)
;; extend a given environment cache with a new binding
(define (Extend id val rest-env)
(lambda (name)
(if (eq? name id)
val
(rest-env name))))
(: lookup : Symbol ENV -> VAL)
;; lookup a symbol in an environment, return its value or throw an
;; error if it isn't bound
(define (lookup name env)
(env name))
(: eval : FLANG ENV -> VAL)
;; evaluates FLANG expressions by reducing them to values
(define (eval expr env)
(: evalN : FLANG -> Number)
(define (evalN e)
(let ([n (eval e env)])
(if (number? n)
n
(error 'eval "got a non-number: ~s" n))))
(: evalF : FLANG -> (VAL -> VAL))
(define (evalF e)
(let ([f (eval e env)])
(if (function? f)
f
(error 'eval "got a non-function: ~s" f))))
(cases expr
[(Num n) n]
[(Add l r) (+ (evalN l) (evalN r))]
[(Sub l r) (- (evalN l) (evalN r))]
[(Mul l r) (* (evalN l) (evalN r))]
[(Div l r) (/ (evalN l) (evalN r))]
[(With bound-id named-expr bound-body)
(eval bound-body
(Extend bound-id (eval named-expr env) env))]
[(Id name) (lookup name env)]
[(Fun bound-id bound-body)
(lambda ([arg-val : VAL])
(eval bound-body (Extend bound-id arg-val env)))]
[(Call fun-expr arg-expr)
((evalF fun-expr) (eval arg-expr env))]))
(: run : String -> VAL) ; no need to convert VALs to numbers
;; evaluate a FLANG program contained in a string
(define (run str)
(eval (parse str) (EmptyEnv)))
;; tests
(test (run "{call {fun {x} {+ x 1}} 4}")
=> 5)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{call add3 1}}")
=> 4)
(test (run "{with {add3 {fun {x} {+ x 3}}}
{with {add1 {fun {x} {+ x 1}}}
{with {x 3}
{call add1 {call add3 x}}}}}")
=> 7)
(test (run "{with {identity {fun {x} x}}
{with {foo {fun {x} {+ x 1}}}
{call {call identity foo} 123}}}")
=> 124)
(test (run "{with {x 3}
{with {f {fun {y} {+ x y}}}
{with {x 5}
{call f 4}}}}")
=> 7)
(test (run "{call {with {x 3}
{fun {y} {+ x y}}}
4}")
=> 7)
(test (run "{with {f {with {x 3} {fun {y} {+ x y}}}}
{with {x 100}
{call f 4}}}")
=> 7)
(test (run "{call {call {fun {x} {call x 1}}
{fun {x} {fun {y} {+ x y}}}}
123}")
=> 124)
Tuesday, February 11th Recursion, Recursion, Recursion
There is one major feature that is still missing from our language: we
have no way to perform recursion (therefore no kind of loops). So far,
we could only use recursion when we had names. In FLANG, the only way
we can have names is through with
which not good enough for recursion.
To discuss the issue of recursion, we switch to a “broken” version of
(untyped) Racket — one where a define
has a different scoping rules:
the scope of the defined name does not cover the defined expression.
Specifically, in this language, this doesn’t work:
(define (fact n)
(if (zero? n) 1 (* n (fact (- n 1)))))
(fact 5)
In our language, this translation would also not work (assuming we have
if
etc):
{if {= n 0} 1 {* n {call fact {- n 1}}}}}}
{call fact 5}}
And similarly, in plain Racket this won’t work if let
is the only tool
you use to create bindings:
(if (zero? n) 1 (* n (fact (- n 1)))))])
(fact 5))
In the broken-scope language, the define
form is more similar to a
mathematical definition. For example, when we write:
(define (G y) (F y))
(G F)
it is actually shorthand for
(define G (lambda (y) (F y)))
(G F)
we can then replace defined names with their definitions:
(define G (lambda (y) (F y)))
((lambda (y) (F y)) (lambda (x) x))
and this can go on, until we get to the actual code that we wrote:
This means that the above fact
definition is similar to writing:
(if (zero? n) 1 (* n (fact (- n 1)))))
(fact 5)
which is not a well-formed definition — it is meaningless (this is a
formal use of the word “meaningless”). What we’d really want, is to take
the equation (using =
instead of :=
)
(if (zero? n) 1 (* n (fact (- n 1)))))
and find a solution which will be a value for fact
that makes this
true.
If you look at the Racket evaluation rules handout on the web page, you
will see that this problem is related to the way that we introduced the
Racket define
: there is a hand-wavy explanation that talks about
knowing things.
The big question is: can we define recursive functions without Racket’s
magical define
form?
Note: This question is a little different than the question of implementing recursion in our language — in the Racket case we have no control over the implementation of the language. As it will eventually turn out, implementing recursion in our own language will be quite easy when we use mutation in a specific way. So the question that we’re now facing can be phrased as either “can we get recursion in Racket without Racket’s magical definition forms?” or “can we get recursion in our interpreter without mutation?”.
Tuesday, February 11th Recursion without the Magic
PLAI §22.4 (we go much deeper)
Note: This explanation is similar to the one you can find in “The Why of Y”, by Richard Gabriel. (It is an “operational semantics” explanation, focusing on how things should work.)
To implement recursion without the define
magic, we first make an
observation: this problem does not come up in a dynamically-scoped
language. Consider the let
-version of the problem:
(let ([fact (lambda (n)
(if (zero? n) 1 (* n (fact (- n 1)))))])
(fact 5))
This works fine — because by the time we get to evaluate the body of
the function, fact
is already bound to itself in the current dynamic
scope. (This is another reason why dynamic scope is perceived as a
convenient approach in new languages.)
Regardless, the problem that we have with lexical scope is still there,
but the way things work in a dynamic scope suggest a solution that we
can use now. Just like in the dynamic scope case, when fact
is called,
it does have a value — the only problem is that this value is
inaccessible in the lexical scope of its body.
Instead of trying to get the value in via lexical scope, we can imitate
what happens in the dynamically scoped language by passing the fact
value to itself so it can call itself (going back to the original code
in the broken-scope language):
(if (zero? n) 1 (* n (self (- n 1)))))
(fact fact 5) ;***
except that now the recursive call should still send itself along:
(if (zero? n) 1 (* n (self self (- n 1))))) ;***
(fact fact 5)
The problem is that this required rewriting calls to fact
— both
outside and recursive calls inside. To make this an acceptable solution,
calls from both places should not change. Eventually, we should be able
to get a working fact
definition that uses just
The first step in resolving this problem is to curry the fact
definition.
(lambda (n) ;***
(if (zero? n)
1
(* n ((self self) (- n 1)))))) ;***
((fact fact) 5) ;***
Now fact
is no longer our factorial function — it’s a function that
constructs it. So call it make-fact
, and bind fact
to the actual
factorial function.
(lambda (n)
(if (zero? n) 1 (* n ((self self) (- n 1))))))
(define fact (make-fact make-fact)) ;***
(fact 5) ;***
We can try to do the same thing in the body of the factorial function:
instead of calling (self self)
, just bind fact
to it:
(lambda (n)
(let ([fact (self self)]) ;***
(if (zero? n)
1
(* n (fact (- n 1))))))) ;***
(define fact (make-fact make-fact))
(fact 5)
This works fine, but if we consider our original goal, we need to get
that local fact
binding outside of the (lambda (n) ...)
— so we’re
left with a definition that uses the factorial expression as is. So,
swap the two lines:
(let ([fact (self self)]) ;***
(lambda (n) ;***
(if (zero? n) 1 (* n (fact (- n 1)))))))
(define fact (make-fact make-fact))
(fact 5)
But the problem is that this gets us into an infinite loop because we’re
trying to evaluate (self self)
too ea(ge)rly. In fact, if we ignore
the body of the let
and other details, we basically do this:
--reduce-sugar-->
(define make-fact (lambda (self) (self self))) (make-fact make-fact)
--replace-definition-->
((lambda (self) (self self)) (lambda (self) (self self)))
--rename-identifiers-->
((lambda (x) (x x)) (lambda (x) (x x)))
And this expression has an interesting property: it reduces to itself, so evaluating it gets stuck in an infinite loop.
So how do we solve this? Well, we know that (self self)
should be
the same value that is the factorial function itself — so it must be a
one-argument function. If it’s such a function, we can use a value that
is equivalent, except that it will not get evaluated until it is needed,
when the function is called. The trick here is the observation that
(lambda (n) (add1 n))
is really the same as add1
(provided that
add1
is a one-argument function), except that the add1
part doesn’t
get evaluated until the function is called. Applying this trick to our
code produces a version that does not get stuck in the same infinite
loop:
(let ([fact (lambda (n) ((self self) n))]) ;***
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define fact (make-fact make-fact))
(fact 5)
Continuing from here — we know that
(remember how we derived fun
from a with
), so we can turn that let
into the equivalent function application form:
((lambda (fact) ;***
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))
(lambda (n) ((self self) n)))) ;***
(define fact (make-fact make-fact))
(fact 5)
And note now that the (lambda (fact) …) expression is everything that
we need for a recursive definition of fact
— it has the proper
factorial body with a plain recursive call. It’s almost like the usual
value that we’d want to define fact
as, except that we still have to
abstract on the recursive value itself. So lets move this code into a
separate definition for fact-step
:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define (make-fact self)
(fact-step ;***
(lambda (n) ((self self) n))))
(define fact (make-fact make-fact))
(fact 5)
We can now proceed by moving the (make-fact make-fact)
self
application into its own function which is what creates the real
factorial:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define (make-fact self)
(fact-step
(lambda (n) ((self self) n))))
(define (make-real-fact) (make-fact make-fact)) ;***
(define fact (make-real-fact)) ;***
(fact 5)
Rewrite the make-fact
definition using an explicit lambda
:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define make-fact ;***
(lambda (self) ;***
(fact-step
(lambda (n) ((self self) n)))))
(define (make-real-fact) (make-fact make-fact))
(define fact (make-real-fact))
(fact 5)
and fold the functionality of make-fact
and make-real-fact
into a
single make-fact
function by just using the value of make-fact
explicitly instead of through a definition:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define (make-real-fact)
(let ([make (lambda (self) ;***
(fact-step ;***
(lambda (n) ((self self) n))))]) ;***
(make make)))
(define fact (make-real-fact))
(fact 5)
We can now observe that make-real-fact
has nothing that is specific to
factorial — we can make it take a “core function” as an argument:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define (make-real-fact core) ;***
(let ([make (lambda (self)
(core ;***
(lambda (n) ((self self) n))))])
(make make)))
(define fact (make-real-fact fact-step)) ;***
(fact 5)
and call it make-recursive
:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define (make-recursive core) ;***
(let ([make (lambda (self)
(core
(lambda (n) ((self self) n))))])
(make make)))
(define fact (make-recursive fact-step)) ;***
(fact 5)
We’re almost done now — there’s no real need for a separate
fact-step
definition, just use the value for the definition of fact
:
(let ([make (lambda (self)
(core
(lambda (n) ((self self) n))))])
(make make)))
(define fact
(make-recursive
(lambda (fact) ;***
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))) ;***
(fact 5)
turn the let
into a function form:
((lambda (make) (make make)) ;***
(lambda (self) ;***
(core (lambda (n) ((self self) n)))))) ;***
(define fact
(make-recursive
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))))
(fact 5)
do some renamings to make things simpler — make
and self
turn to
x
, and core
to f
:
((lambda (x) (x x)) ;***
(lambda (x) (f (lambda (n) ((x x) n)))))) ;***
(define fact
(make-recursive
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))))
(fact 5)
or we can manually expand that first (lambda (x) (x x)) application to
make the symmetry more obvious (not really surprising because it started
with a let
whose purpose was to do a self-application):
((lambda (x) (f (lambda (n) ((x x) n)))) ;***
(lambda (x) (f (lambda (n) ((x x) n)))))) ;***
(define fact
(make-recursive
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))))
(fact 5)
And we finally got what we were looking for: a general way to define
any recursive function without any magical define
tricks. This also
work for other recursive functions:
(define (make-recursive f)
((lambda (x) (f (lambda (n) ((x x) n))))
(lambda (x) (f (lambda (n) ((x x) n))))))
(define fact
(make-recursive
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))))
(fact 5)
(define fib
(make-recursive
(lambda (fib)
(lambda (n) (if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2))))))))
(fib 8)
(define length
(make-recursive
(lambda (length)
(lambda (l) (if (null? l) 0 (+ (length (rest l)) 1))))))
(length '(x y z))
A convenient tool that people often use on paper is to perform a kind of
a syntactic abstraction: “assume that whenever I write (twice foo) I
really meant to write (foo foo)”. This can often be done as plain
abstractions (that is, using functions), but in some cases — for
example, if we want to abstract over definitions — we just want such a
rewrite rule. (More on this towards the end of the course.) The
broken-scope language does provide such a tool — rewrite
extends the
language with a rewrite rule. Using this, and our make-recursive
, we
can make up a recursive definition form:
=> (define f (make-recursive (lambda (f) (lambda (x) E)))))
In other words, we’ve created our own “magical definition” form. The above code can now be written in almost the same way it is written in plain Racket:
(define (make-recursive f)
((lambda (x) (f (lambda (n) ((x x) n))))
(lambda (x) (f (lambda (n) ((x x) n))))))
(rewrite (define/rec (f x) E)
=> (define f (make-recursive (lambda (f) (lambda (x) E)))))
;; examples
(define/rec (fact n) (if (zero? n) 1 (* n (fact (- n 1)))))
(fact 5)
(define/rec (fib n) (if (<= n 1) n (+ (fib (- n 1)) (fib (- n 2)))))
(fib 8)
(define/rec (length l) (if (null? l) 0 (+ (length (rest l)) 1)))
(length '(x y z))
Finally, note that make-recursive is limited to 1-argument functions only because of the protection from eager evaluation. In any case, it can be used in any way you want, for example,
is a function that returns itself rather than calling itself. Using the rewrite rule, this would be:
which is the same as:
in plain Racket.
Tuesday, February 11th The Core of make-recursive
As in Racket, being able to express recursive functions is a fundamental property of the language. It means that we can have loops in our language, and that’s the essence of making a language powerful enough to be TM-equivalent — able to express undecidable problems, where we don’t know whether there is an answer or not.
The core of what makes this possible is the expression that we have seen in our derivation:
which reduces to itself, and therefore has no value: trying to evaluate it gets stuck in an infinite loop. (This expression is often called “Omega”.)
This is the key for creating a loop — we use it to make recursion
possible. Looking at our final make-recursive
definition and ignoring
for a moment the “protection” that we need against being stuck
prematurely in an infinite loop:
((lambda (x) (x x))
(lambda (x) (f (x x)))))
we can see that this is almost the same as the Omega expression — the
only difference is that application of f
. Indeed, this expression (the
result of (make-recursive F) for some F
) reduces in a similar way to
Omega:
((lambda (x) (F (x x))) (lambda (x) (F (x x))))
(F ((lambda (x) (F (x x))) (lambda (x) (F (x x)))))
(F (F ((lambda (x) (F (x x))) (lambda (x) (F (x x))))))
(F (F (F ((lambda (x) (F (x x))) (lambda (x) (F (x x)))))))
...
which means that the actual value of this expression is:
This definition would be sufficient if we had a lazy language, but to
get things working in a strict one we need to bring back the protection.
This makes things a little different — if we use (protect f)
to be a
shorthand for the protection trick,
then we have:
((lambda (x) (x x)) (lambda (x) (f (protect (x x))))))
which makes the (make-recursive F) evaluation reduce to
and this is still the same result (as long as F
is a single-argument
function).
(Note that protect
cannot be implemented as a plain function!)
Tuesday, February 11th Denotational Explanation of Recursion
Note: This explanation is similar to the one you can find in “The Little Schemer” called “(Y Y) Works!”, by Dan Friedman and Matthias Felleisen. (It focuses on what it should be.)
The explanation that we have now for how to derive the make-recursive
definition is fine — after all, we did manage to get it working. But
this explanation was done from a kind of an operational point of view:
we knew a certain trick that can make things work and we pushed things
around until we got it working like we wanted. Instead of doing this, we
can re-approach the problem from a more declarative point of view.
So, start again from the same broken code that we had (using the broken-scope language):
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))
This is as broken as it was when we started: the occurrence of fact
in
the body of the function is free, which means that this code is
meaningless. To avoid the compilation error that we get when we run this
code, we can substitute anything for that fact
— it’s even better
to use a replacement that will lead to a runtime error:
(lambda (n) (if (zero? n) 1 (* n (777 (- n 1)))))) ;***
This function will not work in a similar way to the original one — but
there is one case where it does work: when the input value is 0
(since then we do not reach the bogus application). We note this by
calling this function fact0
:
(lambda (n) (if (zero? n) 1 (* n (777 (- n 1))))))
Now that we have this function defined, we can use it to write fact1
which is the factorial function for arguments of 0
or 1
:
(lambda (n) (if (zero? n) 1 (* n (777 (- n 1))))))
(define fact1
(lambda (n) (if (zero? n) 1 (* n (fact0 (- n 1))))))
And remember that this is actually just shorthand for:
(lambda (n)
(if (zero? n)
1
(* n ((lambda (n)
(if (zero? n)
1
(* n (777 (- n 1)))))
(- n 1))))))
We can continue in this way and write fact2
that will work for n<=2:
(lambda (n) (if (zero? n) 1 (* n (fact1 (- n 1))))))
or, in full form:
(lambda (n)
(if (zero? n)
1
(* n ((lambda (n)
(if (zero? n)
1
(* n ((lambda (n)
(if (zero? n)
1
(* n (777 (- n 1)))))
(- n 1)))))
(- n 1))))))
If we continue this way, we will get the true factorial function, but the problem is that to handle any possible integer argument, it will have to be an infinite definition! Here is what it is supposed to look like:
(define fact1 (lambda (n) (if (zero? n) 1 (* n (fact0 (- n 1))))))
(define fact2 (lambda (n) (if (zero? n) 1 (* n (fact1 (- n 1))))))
(define fact3 (lambda (n) (if (zero? n) 1 (* n (fact2 (- n 1))))))
...
The true factorial function is fact-infinity
, with an infinite size.
So, we’re back at the original problem…
To help make things more concise, we can observe the repeated pattern in
the above, and extract a function that abstracts this pattern. This
function is the same as the fact-step
that we have seen previously:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define fact0 (fact-step 777))
(define fact1 (fact-step fact0))
(define fact2 (fact-step fact1))
(define fact3 (fact-step fact2))
...
which is actually:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define fact0 (fact-step 777))
(define fact1 (fact-step (fact-step 777)))
(define fact2 (fact-step (fact-step (fact-step 777))))
...
(define fact
(fact-step (fact-step (fact-step (... (fact-step 777) ...)))))
Do this a little differently — rewrite fact0
as:
((lambda (mk) (mk 777))
fact-step))
Similarly, fact1
is written as:
((lambda (mk) (mk (mk 777)))
fact-step))
and so on, until the real factorial, which is still infinite at this stage:
((lambda (mk) (mk (mk (... (mk 777) ...))))
fact-step))
Now, look at that (lambda (mk) ...)
— it is an infinite expression,
but for every actual application of the resulting factorial function we
only need a finite number of mk
applications. We can guess how many,
and as soon as we hit an application of 777
we know that our guess is
too small. So instead of 777
, we can try to use the maker function to
create and use the next.
To make things more explicit, here is the expression that is our
fact0
, without the definition form:
fact-step)
This function has a very low guess — it works for 0, but with 1 it
will run into the 777
application. At this point, we want to somehow
invoke mk
again to get the next level — and since 777
does get
applied, we can just replace it with mk
:
fact-step)
The resulting function works just the same for an input of 0
because
it does not attempt a recursive call — but if we give it 1
, then
instead of running into the error of applying 777
:
we get to apply fact-step
there:
and this is still wrong, because fact-step
expects a function as an
input. To see what happens more clearly, write fact-step
explicitly:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
The problem is in what we’re going to pass into fact-step
— its
fact
argument will not be the factorial function, but the mk
function constructor. Renaming the fact
argument as mk
will make
this more obvious (but not change the meaning):
(lambda (mk)
(lambda (n) (if (zero? n) 1 (* n (mk (- n 1)))))))
It should now be obvious that this application of mk
will not work,
instead, we need to apply it on some function and then apply the
result on (- n 1)
. To get what we had before, we can use 777
as a
bogus function:
(lambda (mk)
(lambda (n) (if (zero? n) 1 (* n ((mk 777) (- n 1)))))))
This will allow one recursive call — so the definition works for both
inputs of 0
and 1
— but not more. But that 777
is used as a
maker function now, so instead, we can just use mk
itself again:
(lambda (mk)
(lambda (n) (if (zero? n) 1 (* n ((mk mk) (- n 1)))))))
And this is a working version of the real factorial function, so make it into a (non-magical) definition:
((lambda (mk) (mk mk))
(lambda (mk)
(lambda (n) (if (zero? n) 1 (* n ((mk mk) (- n 1))))))))
But we’re not done — we “broke” into the factorial code to insert that
(mk mk)
application — that’s why we dragged in the actual value of
fact-step
. We now need to fix this. The expression on that last line
is close enough — it is (fact-step (mk mk))
. So we can now try to
rewrite our fact
as:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define fact
((lambda (mk) (mk mk))
(lambda (mk) (fact-step (mk mk)))))
… and would fail in a familiar way! If it’s not familiar enough, just
rename all those mk
s as x
s:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define fact
((lambda (x) (x x))
(lambda (x) (fact-step (x x)))))
We’ve run into the eagerness of our language again, as we did before.
The solution is the same — the (x x)
is the factorial function, so
protect it as we did before, and we have a working version:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define fact
((lambda (x) (x x))
(lambda (x) (fact-step (lambda (n) ((x x) n))))))
The rest should not be surprising now… Abstract the recursive making
bit in a new make-recursive
function:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
(define (make-recursive f)
((lambda (x) (x x))
(lambda (x) (f (lambda (n) ((x x) n))))))
(define fact (make-recursive fact-step))
and now we can do the first reduction inside make-recursive
and write
the fact-step
expression explicitly:
(define (make-recursive f)
((lambda (x) (f (lambda (n) ((x x) n))))
(lambda (x) (f (lambda (n) ((x x) n))))))
(define fact
(make-recursive
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1))))))))
and this is the same code we had before.
Tuesday, February 11th The Y Combinator
Our make-recursive
function is usually called the fixpoint operator
or the Y combinator.
It looks really simple when using the lazy version (remember: our version is the eager one):
(lambda (f)
((lambda (x) (f (x x)))
(lambda (x) (f (x x))))))
Note that if we do allow a recursive definition for Y itself, then the definition can follow the definition that we’ve seen:
(define (Y f) (f (Y f)))
And this all comes from the loop generated by:
This expression, which is also called Omega (the (lambda (x) (x x))
part by itself is usually called omega and then (omega omega)
is
Omega), is also the idea behind many deep mathematical facts. As an
example for what it does, follow the next rule:
"I will say the next sentence twice".
(Note the usage of colon for the first and quotes for the second — what is the equivalent of that in the lambda expression?)
By itself, this just gets you stuck in an infinite loop, as Omega does,
and the Y combinator adds F
to that to get an infinite chain of
applications — which is similar to:
"I will hop on one foot and then say the next sentence twice".
Sidenote: see this SO question and my answer, which came from the PLQ implementation.
Tuesday, February 11th The main property of Y
fact-step
is a function that given any limited factorial, will
generate a factorial that is good for one more integer input. Start with
777
, which is a factorial that is good for nothing (because it’s not a
function), and you can get fact0
as
and that’s a good factorial function only for an input of 0
. Use that
with fact-step
again, and you get
which is the factorial function when you only look at input values of
0
or 1
. In a similar way
is good for 0
…2
— and we can continue as much as we want, except
that we need to have an infinite number of applications — in the
general case, we have:
which is good for 0
…n
. The real factorial would be the result of
running fact-step
on itself infinitely, it is fact-infinity
. In
other words (here fact
is the real factorial):
but note that since this is really infinity, then
= (fact-step fact)
so we get an equation:
and a solution for this is going to be the real factorial. The solution
is the fixed-point of the fact-step
function, in the same sense that
0
is the fixed point of the sin
function because
And the Y combinator does just that — it has this property:
or, using the more common name:
This property encapsulates the real magical power of Y. You can see how
it works: since (Y f) = (f (Y f))
, we can add an f
application to
both sides, giving us (f (Y f)) = (f (f (Y f)))
, so we get:
= (f (f (f ...)))
and we can conclude that
= fact
Tuesday, February 11th Yet another explanation for Y
Here’s another explanation of how the Y combinator works. Remember that
our fact-step
function was actually a function that generates a
factorial function based on some input, which is supposed to be the
factorial function:
(lambda (fact)
(lambda (n) (if (zero? n) 1 (* n (fact (- n 1)))))))
As we’ve seen, you can apply this function on a version of factorial
that is good for inputs up to some n, and the result will be a factorial
that is good for those values up to n+1. The question is what is the
fixpoint of fact-step
? And the answer is that if it maps factₙ
factorial to factₙ₊₁, then the input will be equal to the output on the
infinitieth fact
, which is the actual factorial. Since Y is a
fixpoint combinator, it gives us exactly that answer:
extra] [Tuesday, February 11th Typing the Y Combinator
Typing the Y combinator is a tricky issue. For example, in standard ML you must write a new type definition to do this:
val y = fn f => (fn (T x) => (f (fn a => x (T x) a)))
(T (fn (T x) => (f (fn a => x (T x) a))))
Can you find a pattern in the places where
T
is used? — Roughly speaking, that type definition is;; `t' is the type name, `T' is the constructor (aka the variant)
(define-type (RecTypeOf t)
[T ((RecTypeOf t) -> t)])First note that the two
fn a => ...
parts are the same as our protection, so ignoring that we get:val y = fn f => (fn (T x) => (f (x (T x))))
(T (fn (T x) => (f (x (T x)))))if you now replace
T
withQuote
, things make more sense:val y = fn f => (fn (Quote x) => (f (x (Quote x))))
(Quote (fn (Quote x) => (f (x (Quote x)))))and with our syntax, this would be:
(define (Y f)
((lambda (qx)
(cases qx
[(Quote x) (f (x qx))]))
(Quote
(lambda (qx)
(cases qx
[(Quote x) (f (x qx))])))))it’s not really quotation — but the analogy should help: it uses
Quote
to distinguish functions as values that are applied (thex
s) from functions that are passed as arguments.
In OCaml, this looks a little different:
type 'a t = T of ('a t -> 'a)
# let y f = (fun (T x) -> x (T x))
(T (fun (T x) -> fun z -> f (x (T x)) z)) ;;
val y : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun>
# let fact = y (fun fact n -> if n < 1 then 1 else n * fact(n-1)) ;;
val fact : int -> int = <fun>
# fact 5 ;;
- : int = 120
but OCaml has also a -rectypes
command line argument, which will make
it infer the type by itself:
val y : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun>
# let fact = y (fun fact n -> if n < 1 then 1 else n * fact(n-1)) ;;
val fact : int -> int = <fun>
# fact 5 ;;
- : int = 120
The translation of this to #lang pl
is a little verbose because we
don’t have auto-currying, and because we need to declare input types to
functions, but it’s essentially a direct translation of the above:
[T ((RecTypeOf t) -> t)])
(: Y : (All (A B) ((A -> B) -> (A -> B)) -> (A -> B)))
(define (Y f)
((lambda ([x : (RecTypeOf (A -> B))])
(cases x
[(T x) (x (T x))]))
(T (lambda ([x : (RecTypeOf (A -> B))])
(cases x
[(T x) (lambda ([z : A])
((f (x (T x))) z))])))))
(define fact
(Y (lambda ([fact : (Integer -> Integer)])
(lambda ([n : Integer])
(if (< n 1) 1 (* n (fact (sub1 n))))))))
(fact 5)
It is also possible to write this expression in “plain” Typed Racket,
without a user-defined type — and we need to start with a proper type
definition. First of all, the type of Y should be straightforward: it is
a fixpoint operation, so it takes a T -> T
function and produces its
fixpoint. The fixpoint itself is some T
(such that applying the
function on it results in itself). So this gives us:
However, in our case make-recursive
computes a functional fixpoint,
for unary S -> T
functions, so we should narrow down the type
Now, in the body of make-recursive
we need to add a type for the x
argument which is behaving in a weird way: it is used both as a function
and as its own argument. (Remember — I will say the next sentence
twice: “I will say the next sentence twice”.) We need a recursive type
definition helper (not a new type) for that:
This type is tailored for our use of x
: it is a type for a function
that will consume itself (hence the Rec
) and spit out the value that
the f
argument consumes — an S -> T
function.
The resulting full version of the code:
(define-type (Tau S T) = (Rec this (this -> (S -> T))))
(define (make-recursive f)
((lambda ([x : (Tau S T)]) (f (lambda (z) ((x x) z))))
(lambda ([x : (Tau S T)]) (f (lambda (z) ((x x) z))))))
(: fact : Number -> Number)
(define fact (make-recursive
(lambda ([fact : (Number -> Number)])
(lambda ([n : Number])
(if (zero? n)
1
(* n (fact (- n 1))))))))
(fact 5)