All notes, Tuesday, January 24th ================================ - Intro to CS4400/CS5400 - Intro to Programming Languages - Intro to Racket - Side-note: "Goto Statement Considered Harmful" - Quick Intro to Racket - Lists & Recursion - Some Style - Tail calls - Sidenote on Types - Side-note: Names are important - BNF, Grammars, the AE Language - Simple Parsing - The `match` Form - The `define-type` Form - The `cases` Form - Semantics (= Evaluation) - Side-note: Compositionality - Implementing an Evaluator - Implementing The AE Language - Intro to Typed Racket - Bindings & Substitution - WAE: Adding Bindings to AE - Evaluation of `with` - Formal Specs - Lazy vs Eager Evaluation - de Bruijn Indexes ------------------------------------------------------------------------ # Intro to CS4400/CS5400 [Tuesday, January 10th] * General plan for how the course will go. * Administrative stuff. (Mostly going over the web pages.) ___`https://pl.barzilay.org/`___ ------------------------------------------------------------------------ # Intro to Programming Languages [Tuesday, January 10th] > [PLAI §1] * 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). ------------------------------------------------------------------------ # Intro to Racket [Tuesday, January 10th] * 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). ------------------------------------------------------------------------ ## Side-note: "Goto Statement Considered Harmful" [Tuesday, January 10th] > 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 a `goto` to redirect the flow of execution: Should > all our postconditions consist of a single statement, or should we > only use the arithmetic `if`, which doesn't contain the offensive > `goto`? > > 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! ------------------------------------------------------------------------ # Quick Intro to Racket [Tuesday, January 10th] 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 *number* `42` 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: (if (< 2 3) 10 20) --> 10 because `(< 2 3)` evaluates to `#t`. As a matter of fact, *any* value except for `#f` is considered to be true, so: (if 0 1 2) --> 1 ; all of these are "truthy" (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 (if test consequent) 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 (if #f something) 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, (define (digit-num n) (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: function digit_num(n) { 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: (define (digit-num n) (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: (define (digit-num n) (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`: (cond [test-1 expr-1] [test-2 expr-2] ... [test-n expr-n] [else else-expr]) ------------------------------------------------------------------------ Example for using an `if` expression, and a recursive function: (define (fact n) (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: (define (helper n acc) (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. ------------------------------------------------------------------------ # Lists & Recursion [Tuesday, January 10th] Lists are a fundamental Racket data type. A list is defined as either: 1. the empty list (`null`, `empty`, or `'()`), 2. 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: null (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: null? ; true only for the empty list pair? ; true for any cons cell list? ; this can be defined using the above We can derive `list?` from the above rules: (define (list? x) (if (null? x) #t (and (pair? x) (list? (rest x))))) or better: (define (list? x) (or (null? x) (and (pair? x) (list? (rest x))))) But why can't we define `list?` more simply as (define (list? x) (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): first rest Traditionally called `car`, `cdr`. Also, any `cr` combination for `` 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: (define (list-length list) (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 (define (list-length-helper list len) (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 (define (sum-list l) (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 of `rcons`? * How much time will this take? Does it matter if we use `append` or `rcons`? Redefine `reverse` using tail recursion. * Is the result more complex? (Yes, but not too bad because it collects the elements in reverse.) ------------------------------------------------------------------------ # Some Style [Tuesday, January 10th] When you have some common value that you need to use in several places, it is bad to duplicate it. For example: (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])) 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: (define (how-many-helper b^2 4ac) ; note: valid names! (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 (let ([id expr] ...) expr) For example, (let ([x 1] [y 2]) (+ x y)) But note that the bindings are done "in parallel", for example, try this: (let ([x 1] [y 2]) (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: (define (how-many a b c) (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 for `define` --- 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 nested `if`s (definitely do not force the indentation to make a nested `if` 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 return `2`, 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))))))); ------------------------------------------------------------------------ # Tail calls [Tuesday, January 10th] 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 (if (huh?) (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 (blah (if (huh?) (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: (define (list-length-1 list) (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`: (define (map-1 f l) (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. ------------------------------------------------------------------------ ## Sidenote on Types [Tuesday, January 17th] > 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: map : (A -> B) (Listof A) -> (Listof B) 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: map : (A B -> C) (Listof A) (Listof B) -> (Listof C) Now, here's a hairy example --- what is the type of this function: (define (foo x y) (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: ;; the first `map', consumes a function and two lists 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`: (A B -> C) = (X -> Y) (Listof X) -> (Listof Y) From here we can conclude that A = (X -> Y) B = (Listof X) C = (Listof Y) If we use these equations in `map1`'s type, we get: map1 : ((X -> Y) (Listof X) -> (Listof Y)) (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`: (: foo : (Listof (X -> Y)) (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: (foo (list add1 sub1 add1) (list 1 2 3)) and why this is valid: (foo (list add1 sub1 add1) (map list (list 1 2 3))) ***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: (: foo : (All (X Y) (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: (: foo : (All (X Y) (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: (foo (list add1 sub1 add1) (list (list 1) (list 2) (list 3))) This example works fine, but that's because we wrote the list argument explicitly. If you try to use the exact example above, (foo (list add1 sub1 add1) (map list (list 1 2 3))) 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: (foo (list add1 sub1 add1) (map (inst list Number) (list 1 2 3))) The second facility is `ann`, which can be used to annotate an expression with the type that you expect it to have. (define (foo x y) (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. ------------------------------------------------------------------------ ## Side-note: Names are important [Tuesday, January 17th] 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: s = (-b + sqrt(b^2 - 4*a*c)) / (2*a) instead of x₁ = b * b 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?) ------------------------------------------------------------------------ # BNF, Grammars, the AE Language [Tuesday, January 17th] 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: ::= | + | - 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 ``, which should be one of these: * a number `` * an ``, the text "`+`", and another `` * the same but with "`-`" `` is a terminal: when we reach it in the derivation, we're done. `` 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 `` is (turning it into a `` non-terminal): ::= | + | - ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 `` expression, we first label the rules: ::= (1) | + (2) | - (3) and then we can use them as formal justifications for each derivation step: + ; (2) + ; (1) - + ; (3) - + 3 ; (num) - + 3 ; (1) - + 3 ; (1) 1 - + 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: ::= | ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 Similar solutions can be applied to the `` 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: ::= | + | - and instead of allowing an `` on both sides of the operation, we force one to be a number: ::= | + | - 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: ::= | + | - But what if we want to force precedence? Say that our AE syntax has addition and multiplication: ::= | + | * We can do that same thing as above and add new non-terminals --- say one for "products": ::= | + | ::= | * Now we must parse any AE expression as additions of multiplications (or numbers). First, note that if `` goes to `` and that goes to ``, then there is no need for an `` to go to a ``, so this is the same syntax: ::= + | ::= | * Now, if we want to still be able to multiply additions, we can force them to appear in parentheses: ::= + | ::= | * | ( ) Next, note that `` is still ambiguous about additions, which can be fixed by forcing the left hand side of an addition to be a factor: ::= + | ::= | * | ( ) We still have an ambiguity for multiplications, so we do the same thing and add another non-terminal for "atoms": ::= + | ::= * | ::= | ( ) 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: ::= | ( + ) | ( - ) To prevent confusing Racket code with code in our language(s), we also change the parentheses to curly ones: ::= | { + } | { - } 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: ::= | { + } | { - } (Remember that in a sense, Racket code is written in a form of already-parsed syntax...) ------------------------------------------------------------------------ # Simple Parsing [Tuesday, January 17th] 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: (Add (Sub (Num 3) (Num 4)) (Num 7)) 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: (define-type AE [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]( https://ts-ast-viewer.com/#code/DYUwLgBAghC8EEYDcAoFYCeAHE06NSA).] 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 the `AE` type --- an abstract syntax tree (AST). This is achieved by the following simple recursive function: (: parse-sexpr : Sexpr -> AE) ;; 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`: (: 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)])) 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: (: parse : String -> AE) ;; parses a string containing an AE expression to an AE (define (parse str) (parse-sexpr (string->sexpr str))) ------------------------------------------------------------------------ # The `match` Form [Tuesday, January 17th] The syntax for `match` is (match value [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: (match (list 1 2 3) [x x]) ; evaluates to the list Another simple pattern is a quoted symbol, which matches that symbol. For example: (match foo ['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: (match (list 1 2 3) [(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: > (match '((1 2) (3 4) (5 6) (7 8)) [(list (list x y) ...) (list x y)]) '((1 3 5 7) (2 4 6 8)) A few more useful patterns: id -- matches anything, binds `id' to it _ -- 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: (match x [(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: (match x [(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: > (define (foo x) (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: > (define (foo x) (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 ------------------------------------------------------------------------ # The `define-type` Form [Tuesday, January 17th] 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: (define-type AE [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. ------------------------------------------------------------------------ # The `cases` Form [Tuesday, January 17th] 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` and `cases` > since they are so similar. Try to remember that `match` is for > primitive Racket values (we'll mainly use them for S-expression > values), while `cases` 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: (cases some-ae-value [(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: (cases some-ae-value [(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: (: Add? : AE -> Boolean) ;; 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. ------------------------------------------------------------------------ # Semantics (= Evaluation) [Tuesday, January 17th] > [PLAI §2] 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: ::= ; evaluates to the number | + ; evaluates to the sum of evaluating ; and | - ; ... the subtraction of from (... roughly!) To do this a little more formally: a. eval() = ;*** special rule: translate syntax to value b. eval( + ) = eval() + eval() c. eval( - ) = eval() - eval() Note the completely different roles of the two `+`s and `-`s. In fact, it might have been more correct to write: a. eval("") = b. eval(" + ") = eval("") + eval("") c. eval(" - ") = eval("") - eval("") or even using a marker to denote meta-holes in these strings: a. eval("$") = b. eval("$ + $") = eval("$") + eval("$") c. eval("$ - $") = eval("$") - eval("$") 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 `` for `$` (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 `` 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⟧`: a. [[]] = b. [[ + ]] = [[]] + [[]] c. [[ - ]] = [[]] - [[]] Is there a problem with this definition? Ambiguity: eval(1 - 2 + 3) = ? Depending on the way the expression is parsed, we can get either a result of `2` or `-4`: eval(1 - 2 + 3) = eval(1 - 2) + eval(3) [b] = 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: eval(1 - 2 + 3) = ... = 1 - 2 + 3 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 ``s and then ``s in a similar way: ::= | eval(0) = 0 eval(1) = 1 eval(2) = 2 ... eval(9) = 9 eval() = eval( ) = 10*eval() + eval() 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 ::= | 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 = 33 Changing the order of the last rule works much better: ::= | and then: eval( ) = 10*eval() + eval() * 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? ------------------------------------------------------------------------ ## Side-note: Compositionality [Tuesday, January 17th] 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 ` ` is `10 *` the evaluation of the first, plus the evaluation of the second. In the ` ` 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 `` *syntax*: eval( ) = eval() * 10^length() + eval() 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.) ------------------------------------------------------------------------ # Implementing an Evaluator [Tuesday, January 17th] 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: (define (eval sexpr) (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 {+ 1 {- 3 "a"}} 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. ------------------------------------------------------------------------ # Implementing The AE Language [Tuesday, January 17th] Back to our `eval` --- this will be its (obvious) type: (: eval : AE -> Number) ;; consumes an AE and computes ;; the corresponding number which leads to some obvious test cases: (equal? 3 (eval (parse "3"))) (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")) => 3) (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: (test (parse "{+ {- 3 4} 7}") => (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: (: eval : AE -> Number) (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 (: eval : AE -> Number) (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. (: run : String -> Number) ;; 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") => 3) (test (run "{+ 3 4}") => 7) (test (run "{+ {- 3 4} 7}") => 6) The resulting *full* code is: ;;; ---<<>>----------------------------------------------------- #lang pl #| BNF for the AE language: ::= | { + } | { - } | { * } | { / } |# ;; 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. ------------------------------------------------------------------------ # Intro to Typed Racket [Tuesday, January 24th] 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 ########################################################### (: digit-num : Number -> (U Number String)) (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 (: : ) instead of ";; : " (: 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* return `2`, as expected, but the type checker doesn't know the type of that `2`. 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 of `Nothing` --- a type that is the same as an *empty* union: `(U)`. It's a type that has no values in it --- it fits `error` because it *is* a function that doesn't return any value, in fact, it doesn't return at all. In addition, it means that an `error` expression can be used anywhere you want because it is a subtype of anything at all. * An `else` clause in a `cond` 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 treat `rest` 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 the `call` 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. ------------------------------------------------------------------------ # Bindings & Substitution [Tuesday, January 24th] 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: {* {+ 4 2} {+ 4 2}} 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 corresponding one: {* {+ 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: {with {x {+ 4 2}} {* x x}} We expect to be able to reduce this to: {* 6 6} by substituting 6 for `x` in the body sub-expression of `with`. A little more complicated example: {with {x {+ 4 2}} {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 ------------------------------------------------------------------------ # WAE: Adding Bindings to AE [Tuesday, January 24th] > [PLAI §3] To add this to our language, we start with the BNF. We now call our language "WAE" (With+AE): ::= | { + } | { - } | { * } | { / } | { with { } } | 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 `` 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: (define-type WAE [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: (: parse-sexpr : Sexpr -> WAE) ;; 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: {* 1 2 3} {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: (: parse-sexpr : Sexpr -> WAE) ;; 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: (: 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) ;; 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), > but `With` 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 that `With` and the `with` 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. ------------------------------------------------------------------------ # Evaluation of `with` [Tuesday, January 24th] Now, to make this work, we will need to do some substitutions. We basically want to say that to evaluate: {with {id WAE1} WAE2} we need to evaluate `WAE2` with id substituted by `WAE1`. Formally: eval( {with {id WAE1} WAE2} ) = eval( subst(WAE2,id,WAE1) ) There is a more common syntax for substitution (quick: what do I mean by this use of "syntax"?): eval( {with {id WAE1} WAE2} ) = 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 with `e[x->v]`, and eventually both of > these got combined into `e[v/x]` which is a little confusing in that > the left-to-right order of the arguments is not the same as for the > `subst` 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 identifier `i` in an expression `e` with an > expression `v`, replace all identifiers in `e` that have the same > name `i` by the expression `v`. This seems to work with simple expressions, for example: {with {x 5} {+ x x}} --> {+ 5 5} {with {x 5} {+ 10 4}} --> {+ 10 4} however, we crash with an invalid syntax if we try: {with {x 5} {+ x {with {x 3} 10}}} --> {+ 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: 1. Binding Instance: a binding instance of an identifier is one that is used to name it in a new binding. In our `` syntax, binding instances are only the `` position of the `with` form. 2. 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.) 3. Bound Instance (or Bound Occurrence): an instance of an identifier is bound if it is contained within the scope of a binding instance of its name. 4. Free Instance (or Free Occurrence): 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 identifier `i` in an expression `e` with an > expression `v`, replace all instances of `i` that are not themselves > binding instances with the expression `v`. First of all, check the previous examples: {with {x 5} {+ x x}} --> {+ 5 5} {with {x 5} {+ 10 4}} --> {+ 10 4} still work, and {with {x 5} {+ x {with {x 3} 10}}} --> {+ 5 {with {x 3} 10}} --> {+ 5 10} also works. However, if we try this: {with {x 5} {+ x {with {x 3} x}}} we get: --> {+ 5 {with {x 3} 5}} --> {+ 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 identifier `i` in an expression `e` with an > expression `v`, replace all instances of `i` that are not themselves > binding instances, and that are not in any nested scope, with the > expression `v`. This avoids bad substitution above, but it is now doing things too carefully: {with {x 5} {+ x {with {y 3} x}}} becomes --> {+ 5 {with {y 3} x}} --> {+ 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 identifier `i` in an expression `e` with an > expression `v`, replace all instances of `i` that are not themselves > binding instances, and that are not in any nested scope of `i`, with > the expression `v`. 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 identifier `i` in an expression `e` with an > expression `v`, replace all instances of `i` that are free in `e` > with the expression `v`. Based on this we can finally write the code for it: (: 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) ; 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. ------------------------------------------------------------------------ # Evaluation of `with` (contd.) [Tuesday, January 24th] 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: {with {x E1} E2} 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( {with {x E1} E2} ) = 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 {with {x E1} E2} to E2[E1/x] 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(...) = ... same as the AE rules ... 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: (: 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)])) 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). ;; 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") Putting this all together, we get the following code; trying to run this code will raise an unexpected error... #lang pl #| BNF for the WAE language: ::= | { + } | { - } | { * } | { / } | { with { } } | |# ;; 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 {x 5} {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: (: 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 (subst named-expr from to) ;*** new (subst bound-body from to)))])) And *still* we have a problem... Now it's {with {x 5} {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: (: 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)))])) The complete (and, finally, correct) version of the code is now: ;;; ---<<>>---------------------------------------------------- #lang pl #| BNF for the WAE language: ::= | { + } | { - } | { * } | { / } | { with { } } | |# ;; 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 , `E1', `E2' are s, `x' is some , `y' is a *different* ) 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") ------------------------------------------------------------------------ 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. * BNF: ::= | { + } | { - } | { * } | { / } | { with { } } | 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: 1. Binding Instance. 2. Scope. 3. Bound Instance. 4. Free Instance. * After lots of attempts: > e[v/i] --- To substitute an identifier `i` in an expression `e` > with an expression `v`, replace all instances of `i` that are free > in `e` with the expression `v`. * 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 first `x` 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)])) ------------------------------------------------------------------------ # Formal Specs [Tuesday, January 24th] 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 ``, `E1`, `E2` are ``s, `x` is some ``, `y` is a *different* ``) 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} And a formal definition of `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]) ------------------------------------------------------------------------ # Lazy vs Eager Evaluation [Tuesday, January 24th] 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`: eval({with {x E1} E2}) = eval(E2[eval(E1)/x]) And the matching piece of code is: [(With bound-id named-expr bound-body) (eval (subst bound-body bound-id (Num (eval named-expr))))] How do we make this lazy? In the formal equation: eval({with {x E1} E2}) = eval(E2[E1/x]) and in the code: (: 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))] [(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): > (trace eval) ; (put this in the definitions window) > (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: {with {x {/ 8 0}} 7} 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 {with {x y} 7} 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 {y x} {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, {with {y E1} E2}[v/x] 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 {y x} {with {x 2} {+ x y}}} {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...) ------------------------------------------------------------------------ # de Bruijn Indexes [Tuesday, January 24th] 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 {x 5} {+ x x}} {with {y 5} {+ y y}} or the two Racket function definitions: (define (foo x) (list x x)) (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: {with {x 5} {with {y 6} {+ x y}}} we can use a new "reference" syntax --- `[N]` --- and use this instead of the above: {with 5 {with 6 {+ [1] [0]}}} 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: {with {x 5} {+ x {with {y 6} {+ x y}}}} is translated to: {with 5 {+ [0] {with 6 {+ [1] [0]}}}} (note how `x` appears as a different reference based on where it appeared in the original code.) Even more subtle: {with {x 5} {with {y {+ x 1}} {+ x y}}} is translated to: {with 5 {with {+ [0] 1} {+ [1] [0]}}} 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: subl $8, %esp 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