One more time: strict and nonstrict functions ------------------------------------------------------------------ _|_ means "no value" or "not defined" or "looping" A function, f, is *strict* if f(_|_) = _|_ For a set ("domain") D, let D_ = D union { _|_ }. Figure 3: Say that Loc = {1,2,3} Store = { <a,b,c> | where a,b,c: Int } lookup : Loc x Store -> Int lookup(i, s) = s[i] update : Loc x Int x Store -> Store update(1, n, <a,b,c>) = <n,b,c> (similar for locs 2 and 3) The above operations make no sense with _|_ as an argument. This one does: check : (Store -> Store_) x Store_ -> Store_ check(f, _|_) = _|_ check(f, <a,b,c>) = f(<a,b,c>) Commands in a traditional programming language cannot proceed from _|_: C : Command -> Env -> Store -> Store_ C[[ I = E ]] = lam e. lam s. update(find(I, e), E[[ E ]]e s, s) (this one always computes a Store answer) C[[ loop ]] = lam e. lam s. _|_ (this one always computes no value) C[[ C1 ; C2 ]] = lam e. lam s. check((C[[ C2 ]]e), (C[[ C1]]e s) ) (this one computes C1 and checks the value before allowing C2 to proceed.) Example: C[[ loop; z = 2 ]] init_env (init_store 5) = check( C[[ z = 2 ]]init_env, _|_ ) = _|_ ============================ Can an assignment language be nonstrict? Well, yes, but we must change how the store works: Say that Loc = {0,1,2,...} NStore = { _|_ } union { (m,n)::s | m: Loc, n: Int, s: NStore } an "nstore" is a sequence of updates that are patched onto _|_ using :: init_store = lam n. (1,n)::(2,0)::(3,0)::_|_ lookup : Loc x NStore -> Int_ lookup(m, _|_) = _|_ lookup(m, (m,n)::s') = n lookup(m, (m',n')::s') = lookup(m, s'), if m != m' update : Loc x Int x NStore -> NStore update(m, n, s) = (m,n)::s // this one is important! (Note: if you are suspicious of the phrase, (m,n)::_|_, then please see an alternative coding at the end of this note.) Here is the semantics of the "nonstrict" assignment language with NStore: P : Program -> Int -> Int_ P[[ C . ]] = lam n. lookup(3, C[[ C ]]init_env (init_store(n)) ) C : Command -> Env -> NStore -> NStore C[[ I = E ]] = lam e. lam s. update(find(I, e), E[[ E ]]e s, s) C[[ loop ]] = lam e. lam s. _|_ C[[ C1 ; C2 ]] = lam e. lam s. C[[ C2 ]]e (C[[ C1]]e s) We can calculate that P[[ z = x + 1. ]](5) = lookup(3, C[[ z = x + 1 ]]init_env (init_store(5)) ) = lookup(3, C[[ z = x + 1 ]]init_env ((1,5)::(2,0)::(3,0)::_|_) ) = lookup(3, (3,6)::(1,5)::(2,0)::(3,0)::_|_ ) = 6 We can calculate that P[[ y = x + 1; loop. ]](5) = lookup(3, C[[ y = x + 1; loop ]]init_env (init_store(5)) ) = lookup(3, C[[ loop ]]init_env ((2,6)::(1,5)::(2,0)::(3,0)::_|_) ) = lookup(3, _|_) = _|_ And we can calculate that P[[ loop; z = 2. ]](5) = lookup(3, C[[ loop; z = 2 ]]init_env (init_store(5)) ) = lookup(3, C[[ z = 2 ]]init_env (C[[ loop ]]init_env (init_store(5))) ) = lookup(3, update(3, 2, (C[[ loop ]]init_env (init_store(5)))) ) = lookup(3, (3,2)::(C[[ loop ]]init_env (init_store(5))) ) = lookup(3, (3,2)::_|_) = 2 The last example shows that commands are not strict about propagating _|_. The reason is that the update operation is not strict about the nstore argument. If we wanted to implement this calculation on a conventional machine, we would be forced to calculate the output "backwards" from the "end of the program". This example stimulated thinking in the 1970s on demand-driven or "lazy" computation and led to the development of the "lazy functional language" Haskell. ================== FINAL NOTE: Here is how nonstrict store update was originally defined: NStore = Loc -> Nat_ (stores are truly functions here) lookup(loc, s) = s(loc) update(loc, val, s) = lam loc'. if loc' = loc then val else s(loc') empty = \lam loc. _|_ init_store = \lam n. update(1, n, update(2, 0, update(3, 0, empty)))