X-Git-Url: http://lambda.jimpryor.net/git/gitweb.cgi?p=lambda.git;a=blobdiff_plain;f=week4.mdwn;h=5e219484b7cae7ff81e112bad9481044747c1204;hp=5870a6a3ff8cca5e4ef453ca53c5bc312245c410;hb=7b3b1c578a8c6d50c71ab570a1a49a1c3385ca62;hpb=97ac6ce93d149beb471ad64da9e47e0416efbe82 diff --git a/week4.mdwn b/week4.mdwn index 5870a6a3..5e219484 100644 --- a/week4.mdwn +++ b/week4.mdwn @@ -6,11 +6,11 @@ A: That's easy: let `T` be an arbitrary term in the lambda calculus. If `T` has a fixed point, then there exists some `X` such that `X <~~> TX` (that's what it means to *have* a fixed point). -
-let W = \x.T(xx) in -let X = WW in -X = WW = (\x.T(xx))W = T(WW) = TX -+
+let L = \x. T (x x) in
+let X = L L in
+X ≡ L L ≡ (\x. T (x x)) L ~~> T (L L) ≡ T X
+
Please slow down and make sure that you understand what justified each
of the equalities in the last line.
@@ -173,159 +173,399 @@ so A 4 x is to A 3 x as hyper-exponentiation is to exponentiation...
Is leastness important?
-##The simply-typed lambda calculus##
-
-The uptyped lambda calculus is pure computation. It is much more
-common, however, for practical programming languages to be typed.
-Likewise, systems used to investigate philosophical or linguistic
-issues are almost always typed. Types will help us reason about our
-computations. They will also facilitate a connection between logic
-and computation.
-
-Soon we will consider polymorphic type systems. First, however, we
-will consider the simply-typed lambda calculus. There's good news and
-bad news: the good news is that the simply-type lambda calculus is
-strongly normalizing: every term has a normal form. We shall see that
-self-application is outlawed, so Ω can't even be written, let
-alone undergo reduction. The bad news is that fixed-point combinators
-are also forbidden, so recursion is neither simple nor direct.
-
-#Types#
-
-We will have at least one ground type, `o`. From a linguistic point
-of view, think of the ground types as the bar-level 0 categories, that
-is, the lexical types, such as Noun, Verb, Preposition (glossing over
-the internal complexity of those categories in modern theories).
-
-In addition, there will be a recursively-defined class of complex
-types `T`, the smallest set such that
-
-* ground types, including `o`, are in `T`
-
-* for any types σ and τ in `T`, the type σ -->
- τ is in `T`.
-
-For instance, here are some types in `T`:
-
- o
- o --> o
- o --> o --> o
- (o --> o) --> o
- (o --> o) --> o --> o
-
-and so on.
-
-#Typed lambda terms#
-
-Given a set of types `T`, we define the set of typed lambda terms Λ_T
,
-which is the smallest set such that
-
-* each type `t` has an infinite set of distinct variables, {x^t}_1,
- {x^t}_2, {x^t}_3, ...
-
-* If a term `M` has type σ --> τ, and a term `N` has type
- σ, then the application `(M N)` has type τ.
-
-* If a variable `a` has type σ, and term `M` has type τ,
- then the abstract λ a M
has type σ --> τ.
-
-The definitions of types and of typed terms should be highly familiar
-to semanticists, except that instead of writing σ --> τ,
-linguists (following Montague, who followed Church) write <σ,
-τ>. We will use the arrow notation, since it is more iconic.
-
-Some examples (assume that `x` has type `o`):
-
- x o
- \x.x o --> o
- ((\x.x) x) o
-Excercise: write down terms that have the following types:
+#Sets#
+
+You're now already in a position to implement sets: that is, collections with
+no intrinsic order where elements can occur at most once. Like lists, we'll
+understand the basic set structures to be *type-homogenous*. So you might have
+a set of integers, or you might have a set of pairs of integers, but you
+wouldn't have a set that mixed both types of elements. Something *like* the
+last option is also achievable, but it's more difficult, and we won't pursue it
+now. In fact, we won't talk about sets of pairs, either. We'll just talk about
+sets of integers. The same techniques we discuss here could also be applied to
+sets of pairs of integers, or sets of triples of booleans, or sets of pairs
+whose first elements are booleans, and whose second elements are triples of
+integers. And so on.
+
+(You're also now in a position to implement *multi*sets: that is, collections
+with no intrinsic order where elements can occur multiple times: the multiset
+{a,a} is distinct from the multiset {a}. But we'll leave these as an exercise.)
+
+The easiest way to implement sets of integers would just be to use lists. When
+you "add" a member to a set, you'd get back a list that was either identical to
+the original list, if the added member already was present in it, or consisted
+of a new list with the added member prepended to the old list. That is:
+
+ let empty_set = empty in
+ ; see the library for definitions of any and eq
+ let make_set = \new_member old_set. any (eq new_member) old_set
+ ; if any element in old_set was eq new_member
+ old_set
+ ; else
+ make_list new_member old_set
+
+Think about how you'd implement operations like `set_union`,
+`set_intersection`, and `set_difference` with this implementation of sets.
+
+The implementation just described works, and it's the simplest to code.
+However, it's pretty inefficient. If you had a 100-member set, and you wanted
+to create a set which had all those 100-members and some possibly new element
+`e`, you might need to check all 100 members to see if they're equal to `e`
+before concluding they're not, and returning the new list. And comparing for
+numeric equality is a moderately expensive operation, in the first place.
+
+(You might say, well, what's the harm in just prepending `e` to the list even
+if it already occurs later in the list. The answer is, if you don't keep track
+of things like this, it will likely mess up your implementations of
+`set_difference` and so on. You'll have to do the book-keeping for duplicates
+at some point in your code. It goes much more smoothly if you plan this from
+the very beginning.)
+
+How might we make the implementation more efficient? Well, the *semantics* of
+sets says that they have no intrinsic order. That means, there's no difference
+between the set {a,b} and the set {b,a}; whereas there is a difference between
+the *list* `[a;b]` and the list `[b;a]`. But this semantic point can be respected
+even if we *implement* sets with something ordered, like list---as we're
+already doing. And we might *exploit* the intrinsic order of lists to make our
+implementation of sets more efficient.
+
+What we could do is arrange it so that a list that implements a set always
+keeps in elements in some specified order. To do this, there'd have *to be*
+some way to order its elements. Since we're talking now about sets of numbers,
+that's easy. (If we were talking about sets of pairs of numbers, we'd use
+"lexicographic" ordering, where `(a,b) < (c,d)` iff `a < c or (a == c and b <
+d)`.)
+
+So, if we were searching the list that implements some set to see if the number
+`5` belonged to it, once we get to elements in the list that are larger than `5`,
+we can stop. If we haven't found `5` already, we know it's not in the rest of the
+list either.
+
+This is an improvement, but it's still a "linear" search through the list.
+There are even more efficient methods, which employ "binary" searching. They'd
+represent the set in such a way that you could quickly determine whether some
+element fell in one half, call it the left half, of the structure that
+implements the set, if it belonged to the set at all. Or that it fell in the
+right half, it it belonged to the set at all. And then the same sort of
+determination could be made for whichever half you were directed to. And then
+for whichever quarter you were directed to next. And so on. Until you either
+found the element or exhausted the structure and could then conclude that the
+element in question was not part of the set. These sorts of structures are done
+using **binary trees** (see below).
+
+
+#Aborting a search through a list#
+
+We said that the sorted-list implementation of a set was more efficient than
+the unsorted-list implementation, because as you were searching through the
+list, you could come to a point where you knew the element wasn't going to be
+found. So you wouldn't have to continue the search.
+
+If your implementation of lists was, say v1 lists plus the Y-combinator, then
+this is exactly right. When you get to a point where you know the answer, you
+can just deliver that answer, and not branch into any further recursion. If
+you've got the right evaluation strategy in place, everything will work out
+fine.
+
+But what if you're using v3 lists? What options would you have then for
+aborting a search?
+
+Well, suppose we're searching through the list `[5;4;3;2;1]` to see if it
+contains the number `3`. The expression which represents this search would have
+something like the following form:
+
+ ..................extract_fst ≡ \pair. pair (\x y. x)
-Assume Ω has type τ, and `\x.xx` has type σ. Then
-because `\x.xx` takes an argument of type σ and returns
-something of type τ, `\x.xx` must also have type σ -->
-τ. By repeating this reasoning, `\x.xx` must also have type
-(σ --> τ) --> τ; and so on. Since variables have
-finite types, there is no way to choose a type for the variable `x`
-that can satisfy all of the requirements imposed on it.
+but at a lower level, the pair is still accepting its handler as an argument,
+rather than the handler taking the pair as an argument. (The handler gets *the
+pair's elements*, not the pair itself, as arguments.)
-In general, there is no way for a function to have a type that can
-take itself for an argument. It follows that there is no way to
-define the identity function in such a way that it can take itself as
-an argument. Instead, there must be many different identity
-functions, one for each type.
+> *Terminology*: we'll try to use names of the form `get_foo` for handlers, and
+names of the form `extract_foo` for lifted versions of them, that accept the
+lists (or whatever data structure we're working with) as arguments. But we may
+sometimes forget.
-#Typing numerals#
+The v2 implementation of lists followed a similar strategy:
-Version 1 type numerals are not a good choice for the simply-typed
-lambda calculus. The reason is that each different numberal has a
-different type! For instance, if zero has type σ, then `false`
-has type τ --> τ --> τ, for some τ. Since one is
-represented by the function `\x.x false 0`, one must have type (τ
---> τ --> τ) --> σ --> σ. But this is a different
-type than zero! Because each number has a different type, it becomes
-impossible to write arithmetic operations that can combine zero with
-one. We would need as many different addition operations as we had
-pairs of numbers that we wanted to add.
+ v2list (\h t. do_something_with_h_and_t) result_if_empty
-Fortunately, the Church numberals are well behaved with respect to
-types. They can all be given the type (σ --> σ) -->
-σ --> σ.
+If the `v2list` here is not empty, then this will reduce to the result of
+supplying the list's head and tail to the handler `(\h t.
+do_something_with_h_and_t)`.
+Now, what we've been imagining ourselves doing with the search through the v3
+list is something like this:
+ larger_computation (search_through_the_list_for_3) other_arguments
+That is, the result of our search is supplied as an argument (perhaps together
+with other arguments) to the "larger computation". Without knowing the
+evaluation order/reduction strategy, we can't say whether the search is
+evaluated before or after it's substituted into the larger computation. But
+semantically, the search is the argument and the larger computation is the
+function to which it's supplied.
-
+
+Of course, like everything elegant and exciting in this seminar, [Oleg
+discusses it in much more
+detail](http://okmij.org/ftp/Streams.html#enumerator-stream).
+
+*Comments*:
+
+1. The technique deployed here, and in the v2 lists, and in our implementations
+ of pairs and booleans, is known as **continuation-passing style** programming.
+
+2. We're still building the list as a right fold, so in a sense the
+ application of `f` to the leftmost element `5` is "outermost". However,
+ this "outermost" application is getting lifted, and passed as a *handler*
+ to the next right application. Which is in turn getting lifted, and
+ passed to its next right application, and so on. So if you
+ trace the evaluation of the `extract_head` function to the list `[5;4;3;2;1]`,
+ you'll see `1` gets passed as a "this is the head sofar" answer to its
+ `continue_handler`; then that answer is discarded and `2` is
+ passed as a "this is the head sofar" answer to *its* `continue_handler`,
+ and so on. All those steps have to be evaluated to finally get the result
+ that `5` is the outer/leftmost head of the list. That's not an efficient way
+ to get the leftmost head.
+
+ We could improve this by building lists as left folds when implementing them
+ as continuation-passing style folds. We'd just replace above:
+
+ let make_list = \h t. \f z continue_handler abort_handler.
+ f h z (\z. t f z continue_handler abort_handler) abort_handler
+
+ now `extract_head` should return the leftmost head directly, using its `abort_handler`:
+
+ let extract_head = \lst larger_computation. lst
+ (\hd sofar continue_handler abort_handler. abort_handler hd)
+ junk
+ larger_computation
+ larger_computation
+
+3. To extract tails efficiently, too, it'd be nice to fuse the apparatus developed
+ in these v5 lists with the ideas from [v4](/advanced/#v4) lists.
+ But that also is left as an exercise.
--->