Logistics:

  • Tonight is the last night for quiz regrade requests (Feb. 21)
  • Next homework released this Wednesday, due the following Wednesday (Feb. 28)
  • Second quiz Thursday Feb. 29 / Friday March 1
    • Covers simply-typed lambda calculus and type systems up to the next lecture on sum and product types.
    • Be prepared to make a type system and type checker for a small language.
  • Next homework released today, due next Wednesday (Feb. 28)
  • There is an optional anonymous poll, please take it to give us feedback on how the course is going so far: link here

Goals for today:

  • Review a bit of the quiz (host semantics, call-by-value lambda calculus)
  • Handling recursion in STLC
    • Understandling the letrec syntactic form
  • Handling mutation in STLC
    • Be able to articulate how references can “go wrong” in the STLC with references
    • Understand the type system for references

Quiz #1 review

  • Some general grading philosophy: grades don’t always reflect how well you know the material! There are many reasons for getting a question wrong.
    • This quiz isn’t worth that much. Each quiz is 10% of your grade, so you can still get an A even if you got 50% on this quiz.
  • If you are stuck, rely on the course notes first, then the textbook (PLAI), then try the Internet, but be careful with the Internet!
    • I encourage you to take notes during class. Sometimes I cover things slightly differently in class than I do in the notes, and it can be good to have both.

Host semantics

  • The host semantics are the semanics of the language in which we are implementing our interpreter (i.e., Plait)
  • A change to the host semantics would then be a change to the semantics of Plait.
  • We were asking you to give us any hypothetical change to the semantics of Plait that would cause the interpreter to behave differently on some program.

Reduction rules for the lambda calculus

Consider these lambda terms:

z = (λx. (λy. y))
succ = (λn. (λs. (λz. (s ((n s ) z)))))

Let’s evaluate the question from the quiz:

(succ z)
--> ((λn. (λs. (λz. (s ((n s ) z))))) (λx. (λy. y)))
--> (λs. (λz. (s ((n s ) z))))[n |-> (λx. (λy. y))]
--> (λs. (λz. (s (((λx. (λy. y)) s) z))))
  • We are done stepping because this is a value, even though there are still some applications in the body of the lambda. These were the call-by-value semantics of the lambda calculus. calculus that we went over in class.
  • There are alternative semantics for the lamda calculus that will call functions in the body of a lambda. If you Googled for a lambda calculus tutorial when answering this question, you might have found those instead; those were not the rules we are using, but they are indeed valid (these rules are called “full beta reduction” or “reducing under abstraction”)

Typed recursion

  • See PLAI pg. 129 – 131
  • A fundamental design question in programming language design is what programs are possible to write
  • We mentioned last time that STLC programs cannot loop forever, and therefore do not support arbitrary recursion. This seems undesirable; can we add back in recursion in a way that is still well-typed?
  • Ideed, Plait programs can support recursion and Plait has a type-system. Let’s see how they did it:
; one way is with define. we've been doing this with all of our interpreters.
> (define (loopy x) (loopy x))
; (runs forever...)

; another way is with letrec, which is also convenient.
> (letrec [(loopy (lambda (x) (loopy x)))]
    (loopy 10))
; (runs forever...)
  • So, in order to make it possible to handle recursion in STLC, we need to add a new language feature to the STLC: letrec
  • What is the semantics of letrec? There are actually many ways to give a semantics, but here is one simple way that works by expanding letrec into an infinite sequence of let-bindings. Let’s see how this works.
  • Let’s consider a more interesting recursive function: factorial
(letrec [(factorial (lambda (x) (if (equal? x 0) 1 (* x (factorial (- x 1))))))]
    (factorial 3))
- Number
6
  • The key idea to giving a semantics of letrec is to unfold it into a sequence of lets
  • To simplify our lives, let’s make a version of letrec called letrecN that runs for at most N levels of recursion.
  • Let’s start with letrec0. Let’s define this so that if a function tries to call itself recursively, we raise a runtime error:
(letrec0 [(factorial (lambda (x) (if (equal? x 0) 1 (* x (factorial (- x 1))))))]
    (factorial 3))
; unfold one level by substituting all calls to `factorial` with `error` in the assignment
--> (let [(factorial (lambda (x) (if (equal? x 0) 1 (* x (error 'recursion "")))))] (factorial 3))
--> (factorial 3)[factorial  (lambda (x) (if (equal? x 0) 1 (* x (error 'recursion ""))))]
--> ((lambda (x) (if (equal? x 0) 1 (* x (error 'recursion "")))) 3)
--> (if (equal? 3 0) 1 (* 3 (error 'recursion "")))
--> (* 3 (error 'recursion ""))
--> Error!
  • This raised a 'recursion error because our factorial program has some recursive calls.
  • No problem: let’s use letrec1 which permits at most 1 recursive call. We’ll define this by replacing all recursive calls to an appropriate invocation of letrec0. In this code, we will abbreviate factbody as (lambda (x) (if (equal? x 0) 1 (* x (factorial (- x 1))))):
(letrec1 [(factorial factbody)] (factorial 3))
; replace all recursive calls to `factorial` with `(letrec0 [(factorial factbody)] factorial)` 
--> (let [(factorial (lambda (x) (if (equal? x 0) 1 
       (* x ((letrec0 [(factorial factbody)] factorial) (- x 1))))))] 
       (factorial 3))
--> (if (equal? x 0) 1 (* x ((letrec0 [(factorial factbody)] factorial) (- x 1))))[x  3]
--> (if (equal? 3 0) 1 (* 3 ((letrec0 [(factorial factbody)] factorial) (- 3 1))))
--> (* 3 ((letrec0 [(factorial factbody)] factorial) (- 3 1)))
; now we apply the same process as above for letrec0
--> (* 3 ((let [(factorial (lambda (x) (if (equal? x 0) 1 (* x (error 'recursion "")))))] factorial) 2))
--> (* 3 ((lambda (x) (if (equal? x 0) 1 (* x (error 'recursion "")))) 2))
--> (* 3 (if (equal? x 0) 1 (* x (error 'recursion "")))[x  2])
--> (* 3 (if (equal? 2 0) 1 (* 2 (error 'recursion ""))))
--> Error
  • Clearly this still terminates in an error, but hopefully now we see the pattern: the semantics of (letrec [(x e1)] e2) is defined as: let e1subst be the result of substituting e1[x ↦ (letrec [(x e1)] x)] x)], and then run (let [(x e1subst)] e2).
  • Ponder: Why would the above factorial program terminate under these proposed semantics for letrec?
  • Note: In your homework this week, you will be asked to implement letrec in a different way by using mutable references

Syntax and a letrec interpreter

  • We can implement a tiny letrec interpreter:
(define-type Type
  [TNum]
  [TFun (arg : Type) (ret : Type)])

(define-type Exp
  [varE (s : Symbol)]
  [numE (n : Number)]
  [letrecE (s : Symbol) (t : Type) (e : Exp) (body : Exp)]
  [letE (s : Symbol) (e : Exp) (body : Exp)]
  [lamE (arg : Symbol) (t : Type) (body : Exp)]
  [appE (e1 : Exp) (e2 : Exp)])

; perform e1[x |-> e2]
(subst : (Exp Symbol Exp -> Exp))
(define (subst e1 x e2)
  (type-case Exp e1
             [(varE s) (if (symbol=? s x)
                           e2
                           (varE s))]
             [(numE n) (numE n)]
             [(lamE id typ body)
              (if (symbol=? x id)
                  (lamE id typ body)              ; shadowing case
                  (lamE id typ (subst body x e2)))]
             [(appE e1App e2App)
              (appE (subst e1App x e2)
                    (subst e2App x e2))]
             [(letE s e body)
              (if (equal? s x)
                  (letE s e body)
                  (letE s (subst e x e2) (subst body x e2)))]
             [(letrecE s t e body)
              (if (equal? s x)
                  (letrecE s t e body)
                  (letrecE s t (subst e x e2) (subst body x e2)))]))


(interp : (Exp -> Exp))
(define (interp e)
  (type-case Exp e
    [(varE s) (error 'runtime "unknown symbol")]
    [(numE n) (numE n)]
    [(letE s e body)
     ; run body[s |-> (interp e)]
     (interp (subst body s (interp e)))]
    [(letrecE s t e body)
     ; run (letE s e[s |-> (letrecE s t e s)] body)
     (interp
      (letE s (subst e s (letrecE s t e (varE s)))
            body))]
    [(lamE id typ body) (lamE id typ body)]
    [(appE e1 e2)
     ; run e1 to get (lambda (id) body)
     ; run e2 to get a value argV
     ; run body[id |-> v]
     (letrec [(e1V (interp e1))
              (body (lamE-body e1V))
              (id (lamE-arg e1V))
              (argV (interp e2))]
       (interp (subst body id argV)))]
    ))
  • And, we can try it on a few interesting cases:
(define loopy (letrecE 'f (TFun (TNum) (TNum))
                             (lamE 'x (TNum) (appE (varE 'f) (varE 'x))) (appE (varE 'f) (numE 10))))
; loopy runs forever
> (interp loop)
...

; letrec behaves like let when no recursion is involved (think carefully about why)
> (interp (letrecE 's (TNum) (numE 10) (varE 's))) (numE 10)
10

A typechecker for letrec

  • What should be the typing rule for (letrec (x:τ e1) e2)?
  • Well, x should be in scope in e1, so we should add it to the context while type-checking both e1 and e2
\[\dfrac{Γ ∪ \{x ↦ τ\} ⊢ \texttt{e1} : τ \qquad Γ ∪ \{x ↦ τ\} ⊢ \texttt{e2} : τ2} {Γ ⊢ \texttt{(letrec (x:τ e1) e2) : τ2}}~\text{(T-Letrec)}\]
  • An example derivation of the type of loopy, which makes use of some rules from Lecture 10:
{x ↦ Num, loopy ↦ Num->Num} ⊢ loopy : Num->Num,
{x ↦ Num, loopy ↦ Num->Num} ⊢ x : Num
--------------------------------------------- (T-App)            {loopy ↦ Num -> Num} ⊢ 10 : Num,
{x ↦ Num, loopy ↦ Num->Num} ⊢ (loopy x) : Num                    {loopy ↦ Num -> Num} ⊢ loopy : Num->Num
------------------------------------------------------- (T-Lam)  ---------------------------------------- (T-App)
{loopy ↦ Num -> Num} ⊢ (λx:Num. (loopy x)) : Num -> Num          {loopy ↦ Num -> Num} ⊢ (loopy 10) : Num
-------------------------------------------------------------------------------------------------------- (T-Letrec)
{} ⊢ (letrec loopy (Num -> Num) (λx:Num. (loopy x)) (loopy 10)) : Num
  • A simple typechecker:
(define-type-alias TEnv (Hashof Symbol Type))
(define mt-env (hash empty)) ;; "empty environment"


(define (lookup (n : TEnv) (s : Symbol))
  (type-case (Optionof Type) (hash-ref n s)
             [(none) (error 'type-error "unrecognized symbol")]
             [(some v) v]))

(extend : (TEnv Symbol Type -> TEnv))
(define (extend old-env new-name value)
  (hash-set old-env new-name value))


(define (type-of env e)
  (type-case Exp e
             [(varE s) (lookup env s)]
             [(numE n) (TNum)]
             [(lamE arg typ body)
              (TFun typ (type-of (extend env arg typ) body))]
             [(letE id e body)
              (let [(type-e (type-of env e))]
                (type-of (extend env id type-e) body))]
             [(letrecE id t e body)
              (letrec [(extended-tenv (extend env id t))
                       (type-e (type-of extended-tenv e))
                       (type-body (type-of extended-tenv body))]
                ; check that type-e = t
                (if (equal? type-e t)
                    type-body
                    (error 'type-error "invalid recursive type")))]
             [(appE e1 e2)
              (let [(t-e1 (type-of env e1))
                    (t-e2 (type-of env e2))]
                (type-case Type t-e1
                           [(TFun tau1 tau2)
                            (if (equal? tau1 t-e2)
                                tau2
                                (error 'type-error "invalid function call"))]
                           [else (error 'type-error "invalid function call")]))]))
  • Ponder: What are the guarantees about “going wrong” that this typechecker provides?