Introduction to lambda calculus Part 3 Antti-Juhani Kaijanaho 2017-01-27... 1 Untyped lambda calculus... 2 Typed lambda calculi In an untyped lambda calculus extended with integers, it is required that any value, whether a function or a number, must be treatable as a function or as a number. Thus, in an untyped language, both 5(λx x) and 5+(λx x) must have a specified result (even if it is the undefined value ). In most contexts, however, such usage is a mistake. There are a number of ways to add types to lambda calculus. It rarely makes any sense to do this to the pure lambda calculus, so all my examples will have some extensions to the pure language. For a history of the concept of a type, see Kaijanaho (2015, Section 2.4.2). 2.1 Dynamically typed lambda calculus with integers Here is an abstract syntax for a lambda calculus with integers. Minor changes made afterward. Last changed 2017-01-30 10:58:49+02:00. 1
c, d Const x, y, z Var t, u Term t, u ::= x c t u λx t t + u t u t u t / u For the denotational semantics, dynamic typing means that it must define what terms have type errors, and distinguish between type errors and other kinds of undefinedness. In this language, there are two principal kinds of type errors: calling a numeric value and performing arithmetic on functions. Thus, we must be able to distinguish between numbers and functions at the value level. We need two domains for the denotational semantics: Num is the domain of integers. Fun is the domain of suitable functions D D. Saying that these are domains means that each has an undefined value called bottom. Specifically, there is the undefined integer Num and the undefined function Fun (for which Fun (x) = D holds for all x D). Further, domain theory means that Fun contains only some functions D D, not all of them, but there will be sufficiently many for the purposes of garden variety denotational semantics. We will further assume that all common numeric operations such as addition are defined in Num with the supposition that if any operand is Num, the result is also Num. In other words, the operations are strict in all parameters. We will also assume the existence of a function V : Const Num that takes each literal constant to its numeric value. Now, let us assume that no number is a function and no function is an error, and no error is a number. Then we can define D = Num Fun wrong, D } We need an ultimate bottom D as distinct from the bottoms of each domain to indicate that we do not even know the domain the value belongs to. The value wrong indicates a type error. 2
The denotational semantics is now defined by the following function: E: Term (Var D) D E x σ = σ x (1) E c σ = V c (2) wrong if f Fun or a = wrong E tu σ = (3) f(a) otherwise where f = E t σ and a = E u σ E λx t σ = f, where f : D D E t + u = wrong a + b f(z) = E t (σ[x := z) if a Num or b Num otherwise where a = E t σ and b = E u σ wrong if a Num or b Num E t u = a b otherwise where a = E t σ and b = E u σ wrong if a Num or b Num E t u = a b otherwise where a = E t σ and b = E u σ wrong if a Num or b Num E t / u = Num if a Num and b = 0 otherwise a b where a = E t σ and b = E u σ Example 11 The denotation of (λx x) + 5 is wrong under (5), since E λx x σ is a function, not a numeric value. Reduction uses the standard α and β conversion rules, plus the following arithmetical rules: c + d a v where v = V c + V d (9) c d a v where v = V c V d (10) c d a v where v = V c V d (11) c / d a v (4) (5) (6) (7) (8) where v = V c, if defined (12) V d 3
An arithmetical redex is any term where two numeric constants are operands to an arithmetical operator. For the purposes of reduction order, arithmetical redexes count as redexes. Example 12 Let us compute (λx x+x)(3 3). First, use the normal order: Then the applicative order: (λx x + x)(3 3) β (3 3) + (3 3) a 9 + (3 3) a 9 + 9 a 18 (λx x + x)(3 3) a (λx x + x)9 β 9 + 9 a 18 Exercise 6 Compute (λx (λy (λx x + y))(2 x)) 3 using reductions. 2.2 A model of Lisp The essence of Lisp (see e. g. McCarthy 1960, 1981; McCarthy et al. 1966; Seibel 2005) is a dynamically typed lambda calculus with several extensions: There are atoms, which are akin to constants; every atom has a corresponding atom value which is distinct from every other value. There are at least two atoms: T and NIL. Any two values can be put into an ordered pair, which is itself a value. There is a guarded term for evaluating a term only if the value of another term is not NIL There is a way to combine guarded terms so that it is possible to choose between multiple terms. There is a mechanism for defining values by recursive computation. A real Lisp will also support assignable variables, side effects such as I/O and in-place modification of data structures, metaprogramming using macros, and many other features omitted from this model. 4
Thus, we arrive at the following abstract syntax: a, b, c Atom x, y, z Var t, u Term t, u ::= x a t u λx t µx t recursively defined value t, u ordered pair g g GuardedTerm g ::= t u term guarded by a condition g g 1 g 2 combined guarded term Before defining its semantics or behavior formally, I will explain informally some of the newer constructs. First, µx t (called a label expression in traditional Lisp and a fixpoint term by theorists) is intended to describe recursion, so the value of µx t is the value of t evaluated with x standing for the value of t. Second, a guarded term (t 1 u 1 t 2 u 2 t n u n ) tries each t i in order until one of them evaluates to something other than NIL, in which case the value of the whole guarded term is the value of the corresponding u i ; if all t i evaluate to NIL, then the whole guarded term evaluates to NIL. There is an idiom to chain pairs to form a linked list terminated by the atom NIL, so a, b, c, NIL is a linked list of a, b, and c. It is so ubiquitous an idiom that we will use the following syntactic sugar: [t 1, t 2,..., t n means the same as t 1, t 2,... t n, NIL.... A Lisp program presumes some standard atoms and variables (standing for functions). The standard variables are expected to be defined in the global environment. The most important ones are atoms representing numbers, and the following variables standing for functions: EQ p presumes that p evaluates to a list of atoms; it returns T if they are all the same atom and NIL otherwise. ATOM p returns T if p is an list consisting of a single atom and NIL otherwise. CONS p presumes that p evaluates to a list of two elements and returns a pair of those elements. 5
CAR p presumes that p evaluates to a pair and returns its left component. CDR p presumes that p evaluates to a pair and returns its right component. ADD l presumes that l evaluates to a list of atoms representing numbers and returns their sum. SUB l presumes that l evaluates to a list of atoms representing numbers and subtracts from the first all the rest, and returns the result. MUL l presumes that l evaluates to a list of atoms representing numbers and returns their product. DIV l presumes that l envaluates a list of atoms representing numbers and divides the first by the second, the result by the third, and so on, and returns the final result. Example 13 We may write the factorial function as follows: µf λx (EQ[x, 0 1 T MUL[x, f(sub[x, 1)) For a denotational semantics, we need a domain D that contains all the atoms in Atom, including T and NIL, enough functions D D (which will all belong to the subset Fun), enough pairs from D D (which will all belong to the subset Pair), the type error value wrong, and the undefined value. (Enough in this case means that D must satisfy the requirements of domain theory.) Further, there shall be a domain D f = D fail}, which is used to control selection among guarded terms. The denotational semantics is defined by the following two functions: E: Term (Var D) D E x σ = σ x (13) E a σ = a (14) 6
E tu σ = wrong f(v) if f = or v = else if f Fun or v = wrong otherwise where f = E t σ and v = E u σ E λx t σ = f where f : D D f(z) = E t (σ[x := z) (15) (16) E µx t σ = v where v = E t (σ[x := v) (17) if v = or w = E t, u σ = wrong if v = wrong or w = wrong (18) (v, w) otherwise where v = E t σ and w = E u σ NIL if v = fail E g σ = v otherwise where v = G g σ (19) G: Term (Var D) D f if v = wrong if v = wrong G t u σ = fail if v = NIL E u σ otherwise where v = E t σ G g 2 σ if v = fail G g 1 g 2 σ = v otherwise where v = G g 1 σ (20) (21) The set of free variables and variable substitution are largely similarly computed as with ordinary lambda calculus, except that µx t binds x the same way as λx t does. Exercise 7 Define F V : (Term GuardedTerm) P (Var) and variable substitution (t[x := u) formally for this model of Lisp. Note that you should avoid variable capture even though this was not traditionally done in Lisp. Reduction rules are a bit more complicated. In the rules below, v stands for a term on which no more reductions can be performed. We start with 7
the basic rule of β reduction, restricted to applicative order (requiring the reduction of the argument first): 1 (λx t)v t[x := v (22) Since the x in a recursion term stands for the term itself, we can simply move it inward to simulate one step of recursion. µx t t[x := µx t (23) The reduction rules for guarded terms are suggested by their informal semantics: (NIL u g) g (24) (v u g) u if v NIL (25) (NIL u) NIL (26) (v u) u if t NIL (27) Lisp s traditional reduction order is weak applicative order reduction from left to right: tu t u if t t (28) vu vu if u u (29) t, u t, u if t t (30) v, u v, u if u u (31) (t u g) (t u g) if t t (32) (t u) (t u) if t t (33) Finally, we would need rules for the standard functions, but I will omit them here (you may use them informally to make reduction steps once the arguments are fully reduced). Example 14 Let us compute (µf λx (EQ[x, 0 1 T MUL[x, f(sub[x, 1))) 1 using the reduction rules: ( µf λx ( )) 1 I will no longer subscript the arrow with the type of the reduction. 1 8
λx 1 T MUL x, µf λx (SUB [x, 1) by (23), (28) EQ [1, 0 1 T MUL 1, µf λx (SUB [1, 1) by (22) NIL 1 T MUL 1, µf λx (SUB [1, 1) by EQ, (32) ( [ ( ( )) ) T MUL 1, µf λx (SUB [1, 1) by (24) [ ( ( )) MUL 1, µf λx (SUB [1, 1) by (27) MUL 1, λx (SUB [1, 1) T MUL x, µf λx (SUB [x, 1) by (23), (28), (31),(29) MUL 1, λx T MUL x, µf λx (SUB [x, 1) by SUB, (29), (31),(29) EQ [0, 0 1 MUL 1, T MUL 0, µf λx (SUB [0, 1) by (22), (31),(29) T 1 MUL 1, T MUL 0, µf λx (SUB [0, 1) by EQ, (32), (31),(29) MUL [1, 1 by (25), (31),(29) 1 by MUL There is, however, a further interesting aspect of Lisp. There is a traditional concrete syntax for Lisp data, called S-expressions. The basic idea is that a list is written by listing its elements one by one inside parentheses with nothing (except whitespace if needed) in between; atoms are written as they are. Thus, the list [a, b, [c, d, e is written in S-expression format as (a b (c d e)). Further, a pair is written by listing its two elements in parentheses with a period in between: a, b is written as (a. b). More generally, a sequence of pairs that would be a list except that it terminates in something other than NIL is written like a list except that the terminating thing is separated from the rest of the list with a period; thus a, b, c, d is written as (a b c. d) (this is called a improper list). Atoms are written as themselves. Now, there is a standard way to represent Lisp terms as lisp data, expressed as S-expressions (must be applied recursively): 9
My notation Standard S-expression form t[u 1,..., u n (t u 1 u n ) λx t (LAMBDA x t) µx t (LABEL x t) a (QUOTE a) t, u (QUOTE (t. u)) [t 1,..., t n (QUOTE (t 1 t n ) (t 1 u 1 t n u n ) (COND (t 1 u 1 ) (t n u n )) Further, abstractions are usually written (LAMBDA (X Y Z) ( )) or sometimes even (LAMBDA (X Y. Z) ( )), which allows the S-expression programmer to give names to some or all of the elements of an argument list given to the function. My notation does not support that. Since the QUOTEs get very cumbersome very fast, there is a common abbreviation: we can write instead of (QUOTE ). Quoting of an atom that does not look like a variable (such as a numeric constant) is not necessary. Similarly, do not nest QUOTEs: (QUOTE a (b c) d) is the nested list [a, [b, c, d In classical Lisp, S-expressions were written in upper case letters. Nowadays lower-case letters are more commmonly used, and S-expressions are usually treated as case insensitively (so LAMBDA and lambda are the same atom). Example 15 Here is the factorial function of Example 13 in S-expression form: (label f (lambda (x) (cond ((eq x 0) 1) ( t (mul x (f (sub x 1))))))) Given all this, we can write a Lisp function that reads a Lisp term and an environment represesnted as Lisp data, and performs the reduction. It is not even particularly hard. This function, called eval, is the first ever definitional interpreter written. Practical Lisps usually provide a read-eval-print loop (repl): a command line interface where terms written as S-expressions are input by the user, then evaluated by the Lisp implementation, and finally the value of the expression is printed. Lisps also provide various ways to modify the top-level environment which contains the standard functions for example (define 10
f (lambda )) in Scheme. This is the way programs are also written they are essentially scripts of repl sessions, at least in simple cases. Exercise 8 Write, using either my abstract syntax notation or S-expression notation, a Lisp function that 1. counts the number of elements in a list 2. computes the average of numbers in a list 3. reverses a list You may also use the concrete syntax of a Lisp dialect implementation (but do not use its library funtions beyond the equivalents of the functions listed in this document). In that case, specify which dialect and implementation you used. References Kaijanaho, Antti-Juhani (2015). Evidence-Based Programming Language Design. A Philosophical and Methodological Exploration. Jyväskylä Studies in Computing 222. University of Jyväskylä. url: http://urn.fi/urn: ISBN:978-951-39-6388-0. McCarthy, John (1960). Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I. In: Communications of the ACM 3.4, pp. 184 195. doi: 10.1145/367177.367199. (1981). History of LISP. In: History of programming languages I. New York, NY, USA: ACM, pp. 173 185. isbn: 0-12-745040-8. doi: 10.1145/ 800025.1198360. McCarthy, John et al. (1966). LISP 1.5 Programmer s Manual. 2nd ed. MIT Press. Seibel, Peter (2005). Practical Common Lisp. Apress. url: http://www. gigamonkeys.com/book/. 11