5 Lecture 2 – Modeling a Language, Continued
5.1 A little review
Last lecture (Lecture 1 – Introduction to Rebuilding the Universe), we introduced two Axioms and a small formal system from which we can rebuild the universe, and formally defined a collection of expressions for a language. The Axioms are implication and induction, and we use judgments to define new mathematical theories from these two axioms.
Using this formal system, we defined judgments that represent simple data types, like the natural numbers and booleans. We can define lots of data types this way. In class, we saw how to define another common data type: lists. Intuitively, a list is either empty, or contains some elements.
------- [Empty] |
() is List |
|
--------[1-Element] |
(any_1) is List |
|
|
--------[2-Element] |
(any_1 any_2) is List |
|
--------[3-Element] |
(any_1 any_2 any_3) is List |
|
... |
------- [Empty] |
() is List |
|
any2 is List |
-------- [Cons] |
(cons any1 any2) is List |
This is essentially similar to how we define the natural numbers.
5.2 Making Formal Judgments Really Formal
Trust Me—
I’m a professor Learn some well-established mathematical theory that a large number of mathematicians have agreed is a good theory for a long time, such as set theory, and prove that your new formalism is sound with respect to that theory. Since mathematicians have been unable to break the original theory, and your theory is sound, probably your theory is formal system.
Run it on a computer
Thankfully, there exists a programming language that has essentially the same
underlying Axioms as this class.
We can use PLT Redex to write judgments, build derivations, and check derivations.
This can be very useful for experimenting with judgments—
To use Redex, make sure you have Racket installed, then install Redex on the command line using raco pkg install redex.
> (require redex/reduction-semantics (only-in redex/reduction-semantics (define-language define-universe)))
> (define-universe U1)
> (define-judgment-form U1 #:contract (Is-Bool any_1) [---------- "True" (Is-Bool true)] [---------- "False" (Is-Bool false)])
> (redex-match? U1 any (term true)) #t
> (redex-match? U1 any 'false) #t
> (redex-match? U1 (any_1 any_2) (term (true false))) #t
> (redex-match? U1 (any_1 any_1) (term (true false))) #f
> (redex-match? U1 (any_1 any_1) '(true true)) #t
-------- [True] |
true is Bool |
> (derivation '(Is-Bool true) "True" (list)) (derivation '(Is-Bool true) "True" '())
> (test-judgment-holds Is-Bool (derivation '(Is-Bool true) "True" (list)))
> (define-judgment-form U1 #:contract (Is-Nat any) [----------- "Zero" (Is-Nat z)] [(Is-Nat any) ---------- "Add1" (Is-Nat (s any))])
> (test-judgment-holds Is-Nat (derivation '(Is-Nat (s z)) "Add1" (list (derivation '(Is-Nat z) "Zero" (list)))))
> (define-judgment-form U1 #:contract (Is-Exp any) [---------- "E-Zero" (Is-Exp z)] [(Is-Exp any) --------------- "E-Add1" (Is-Exp (s any))] [(Is-Exp any_1) (Is-Exp any_2) ------------------------ "E-Plus" (Is-Exp (any_1 + any_2))] [------------- "E-True" (Is-Exp true)] [------------- "E-False" (Is-Exp false)] [(Is-Exp any_1) (Is-Exp any_2) (Is-Exp any_3) ------------------------------------------ "E-If" (Is-Exp (if any_1 then any_2 else any_3))])
> (test-judgment-holds Is-Exp (derivation '(Is-Exp (s z)) "E-Add1" (list (derivation '(Is-Exp z) "E-Zero" (list)))))
5.3 BNF Grammars – A short-hand for simple judgments
Anyone familiar with compilers or programming languages may have seen an
alternative method of describing syntax before—
b | = | ::= | ||
| | true | |||
| | | false] [n ::= z | | |||
| | (s n) | |||
l | = | ::= | ||
| | () | |||
| | | (cons any l)] [e ::= n | | |||
| | b | |||
| | | (e_1 + e_2) | | |||
| | (if e_1 then e_2 else e_3) |
> (define-universe U2 [b ::= true false] [n ::= z (s n)] [l ::= () (cons any l)] [e ::= n b (e_1 + e_2) (if e_1 then e_2 else e_3)])
> (redex-match? U2 b 'true) #t
> (redex-match? U2 n 'z) #t
> (redex-match? U2 n '(s z)) #t
> (redex-match? U2 e '(s z)) #t
> (redex-match? U2 (e_1 e_2) '((s z) true)) #t
5.4 Modeling Operatings on Expressions
Until now, we have only brought things into existance in our universe.
We declare that the symbols true and z and if exist
in our universe, and are "Expressions", whatever that means.
But they have no meaning at all.
They are merely symbols that satsify some formal judgment.
To give them meaning, we need to define operations on expressions—
The most common operation we want to define expressions in a language is
evaluation.
We want to know what an expression computes—
In programming languages, it’s common to start defining evaluation by defining one step of reduction. The reduction judgment, commonly written with a single right arrow →, defines a single step of reduction. What happens when an expression that eliminates a value has a value to eliminate. We can define this on paper as the following judgment.
|
------------ "Step-If-True" |
(if true then e_1 else e_2) → e_1 |
|
------------ "Step-If-False" |
(if false then e_1 else e_2) → e_2 |
|
------------- "Step-Add-Zero" |
(z + e) → e) |
|
------------- "Step-Add-Zero" |
((s e_1) + e_2) → (e_1 + (s e_2)) |
By defining this judgment, we begin to give meaning to expressions. Earlier, we define true to be a Bool, but it had no interpretation. We could not reasonably think of it as representing anything at all. However, by declaring that (if true then e1 else e2) steps to e1, we declare that true formally represents our intuition for "something that is true". Similarly, by relating the expressions (z + e) and e, we declare that z behaves like the natural number 0 if we consider + to be the mathematical addition function on natural numbers.
------------ "Step-Add-Zero" |
(z + (s z)) → (s z) |
Note that to interpret this as a proof of things we understand intuitively, like 0 and addition, we must assume an interpretation of the symbols we wrote down. This is fine as long as our interpretation is consisistent.
> (define-judgment-form U2 #:contract (→ e e) [----------------- "Step-If-True" (→ (if true then e_1 else e_2) e_1)] [----------------- "Step-If-False" (→ (if false then e_1 else e_2) e_2)] [-------------- "Step-Add-Zero" (→ (z + e) e)] [------------------------------------ "Step-Add-Add1" (→ ((s e_1) + e_2) (e_1 + (s e_2)))])
> (test-judgment-holds → (derivation '(→ (z + (s z)) (s z)) "Step-Add-Zero" (list)))
> (test-judgment-holds → (derivation '(→ (if true then z else (s z)) z) "Step-If-True" (list)))
5.5 Modeling Evaluation
The single-step reduction judgment forms the basis for an interpreter. We can model an interpreter, formally, as a judgment that applies each of the above reductions over and over and over again, until there are none left. Programming languages people sometimes call this the reflexive, transitive, compatible closure of the reduction judgment. I don’t know why, since these terms do not exist in our universe, but it’s good to know.
> (define-judgment-form U2 #:contract (→* e_1 e_2) [------------- "Refl" (→* e_1 e_1)] [(→ e_1 e_2) ----------------- "Step" (→* e_1 e_2)] [(→* e_1 e_2) (→* e_2 e_3) ------------------ "Trans" (→* e_1 e_3)] [(→* e_1 e_11) ------------------- "If-Compat-e1" (→* (if e_1 then e_2 else e_3) (if e_11 then e_2 else e_3))] [(→* e_2 e_21) ------------------- "If-Compat-e2" (→* (if e_1 then e_2 else e_3) (if e_1 then e_21 else e_3))] [(→* e_3 e_31) ------------------- "If-Compat-e3" (→* (if e_1 then e_2 else e_3) (if e_1 then e_2 else e_31))] [(→* e_1 e_11) --------------------- "Plus-Compat-e1" (→* (e_1 + e_2) (e_11 + e_2))] [(→* e_2 e_21) --------------------- "Plus-Compat-e2" (→* (e_1 + e_2) (e_1 + e_21))])
To model the interpreter, we define the judgment →*. Intuitively, this judgment applies the → judgment any number of times, to any sub-expression. We start by declaring that an expression can take no steps, or, step to itself (→* e_1 e_1). Next, we have a rule saying that if an expression steps in the → judgment, then it can step in the →* judgment. That is, any expression can take a single step. Then, we have a rule that says we can evaluate from e_1 to e_2, and then continue evaluating e_2 to e_3.
I screwed up this explanation in lecture. In lecture, I claimed we had to modify the rule. This isn’t necessarily true, and the modification I made ended up giving us the wrong judgment.
The rest of the rules are called "compatibility" rules. We need one rule for each sub-expression, declaring that the interpreter is allowed to evaluate the sub-expressions.
With this judgment, we can now prove that more complex equations between expressions. For example, we can prove that (if true then ((s z) + (s z)) else false) evaluates to 2. First, we interpret →* as "evaluates". Then, we interpret (s (s z)) as 2. We already assumed earlier that (s z) is 1 and + is addition. Now we build the derivation
> (define one-plus-one-equals-two (derivation '(→* ((s z) + (s z)) (s (s z))) "Trans" (list (derivation '(→* ((s z) + (s z)) (z + (s (s z)))) "Step" (list (derivation '(→ ((s z) + (s z)) (z + (s (s z)))) "Step-Add-Add1" (list)))) (derivation '(→* (z + (s (s z))) (s (s z))) "Step" (list (derivation '(→ (z + (s (s z))) (s (s z))) "Step-Add-Zero" (list)))))))
> (test-judgment-holds →* one-plus-one-equals-two)
> (test-judgment-holds →* (derivation '(→* (if true then ((s z) + (s z)) else false) (s (s z))) "Trans" (list (derivation '(→* (if true then ((s z) + (s z)) else false) ((s z) + (s z))) "Step" (list (derivation '(→ (if true then ((s z) + (s z)) else false) ((s z) + (s z))) "Step-If-True" (list)))) one-plus-one-equals-two)))
The same derivation in on-paper notation is below:
Lemma 1 (One Plus One Equals Two): ((s z) + (s z)) →* (s (s z))
--------------------------------- [Step-Add-Add1] ------------------------------- [Step-Add-Zero] |
((s z) + (s z)) → (z + (s (s z))) (z + (s (s z))) → (s (s z)))) |
---------------------------------- [Step] ------------------------------ [Step] |
((s z) + (s z)) →* (z + (s (s z))) (z + (s (s z))) →* (s (s z))) |
------------------------------------------------------------------------------------- [Trans] |
((s z) + (s z)) →* (s (s z)) |
|
|
------------------------------------------------------------ [Step-If-True] |
(if true then ((s z) + (s z)) else false) → ((s z) + (s z)) Lemma 1 |
------------------------------------------------------------- [Step] ------------------------------ |
(if true then ((s z) + (s z)) else false) →* ((s z) + (s z)) ((s z) + (s z)) →* (s (s z)) |
------------------------------------------------------------------------------------- [Trans] |
(if true then ((s z) + (s z)) else false) →* (s (s z)) |
|