X-Git-Url: http://lambda.jimpryor.net/git/gitweb.cgi?p=lambda.git;a=blobdiff_plain;f=topics%2Fweek3_unit.mdwn;h=548a58d514288dd70834e85b4bd94c4b02c62935;hp=f3f61b4c7c34d79dfaa568a0133b665d2554e900;hb=c04c0731a576ea9e9fcacaf6981e556546ae312d;hpb=010e2ff488d153fbf691ab5f5e42190d176e0d04 diff --git a/topics/week3_unit.mdwn b/topics/week3_unit.mdwn index f3f61b4c..548a58d5 100644 --- a/topics/week3_unit.mdwn +++ b/topics/week3_unit.mdwn @@ -1,4 +1,4 @@ -[[!TOC]] +[[!toc]] What is *unit*? =============== @@ -11,12 +11,12 @@ In the other notes, we discussed how to encode such data structures in the Lambd The component `Cyan ()` says here is one variant, that we will associate with the *tag* `Cyan`, and this variant has no parameters. Here we write that as `()`, for better consistency with the final case where the variant has multiple parameters, but different notational conventions, such as omitting the `()`, would also be possible. The final component `Gray (Number, Number)` says the last variant is associated with the tag `Gray`, and also specifies that this variant has two parameters, and that we expect each of them to be a number. The vertical bar `|` is just a syntactic separator, like a comma. -These "tags" are also called **constructors**, or more specifically "data constructors". (Later we will encounter other kinds of constructors. But "constructor" without any modifiers or special context can be assumed to mean these things.) The reason they are called that is that the way to specify some *instance* of this data structure is to write things like: +These "tags" are also called **constructors**, or more specifically "data constructors". (Later we will encounter other kinds of constructors. But "constructor" without any modifiers or special context can be assumed to mean these things.) The reason they are called that is that the way to specify *some instance* of this data structure is to write: Cyan () Gray (5, 0) -These may look like function applications, but they're not. They're canonical "constructions" of those instances of the data structure. Notice that in the `Gray` case, we construct an instance using specific numbers; whereas in the description of the data structure itself, we used not specific numbers but instead the general type `Number`. +These may look like function applications, but they're not. They're canonical *constructions* of those instances of the data structure. Notice that in the `Gray` case, we construct an instance using specific numbers; whereas in the description of the data structure itself, we used not specific numbers but instead the general type `Number`. (Here too other conventions would be possible, such as, again, omitting the `()` after the `Cyan`. Here we also follow the general convention of Haskell and OCaml in capitalizing the alphabetic names of constructors; whereas, on the other hand, variables bound to functions begin with lowercase letters. In fact, in both languages there are also some constructors written outside this convention. Our friends that we express in Kapulet as `[]` and `&` can be thought of as constructors, too. Haskell expresses those as `[]` and `:` and OCaml as `[]` and `::`. Also, as we said in the other notes, we might think of the *boolean truth-values* as variants in a data structure. Following the conventions here, the most consistent nomenclature would then be to designate them `True ()` and `False ()`. Haskell sort-of does that, but omits the `()`. OCaml also omits the `()` and in this unique case expresses the variants all lower-case rather than capitalized.) @@ -86,7 +86,7 @@ and those would mean the same as: Quadruple (5, 0, 'false, 12) -Similarly, `Quadruple (5, 0, 'false, (), 12)` would also mean the same; though again you may not be able to imagine cases where it's useful to write it in that longer form. On the other hand, you could not say `Quadruple (5, 0, 'false, Unit (), 12)`, because that would be trying to construct a quadruple out of *five* values. +Similarly, `Quadruple (5, 0, 'false, (), 12)` would also mean the same; though again you may not be able to imagine cases where it's useful to write it in that longer form. On the other hand, you could not say `Quadruple (5, 0, 'false, Unit (), 12)`, because that would be trying to construct a *quad*ruple out of *five* values. As I said, there is some conceptual benefit to having both the heavyweight and the lightweight notion of a triple, and the notions of `Unit ()` versus `()` are the corresponding, limiting cases of these. Our discussion below of the ways in which *unit* could be useful really applies to *both* of Kapulet's `Unit ()` and Kapulet's `()`. It's just that which of these notions you focus on will affect how you conceptualize what's going on. If I say: @@ -150,7 +150,7 @@ and expect it to work the same as `(f 5 0 #f)`. Chicken will just use the first Instead, Scheme generally works with heavier-weight collections. But which ones? Let's focus on triples first (or really any *n*-tuple, for *n* at least two), where the answer is already complex. It will become even more so in the case of unit(s). -Scheme has two parallel notions for expressing longer *n*-tuples. The more straightforward one is called a *vector*. You can build a vector in Scheme like this: +Scheme has two parallel notions for expressing longer *n*-tuples. The more straightforward one is called a **vector**. You can build a vector in Scheme like this: (vector 5 0 #f) @@ -160,11 +160,12 @@ and Scheme will display the result like this: --- perhaps with a further single-quote prefix, depending on your configuration. It's also possible to specify a vector using that latter syntax. Vectors are like tuples in Kapulet, Haskell, and OCaml, in that their different elements can be of heterogenous types. They are like Kapulet's heavyweight tuples in that they can be assigned to single variables, can be discrete elements in other structures, including other vectors, and so on. Scheme vectors are *unlike* the tuple structures in the other languages in that they are usually *mutable*: one and numerically the same vector container can contain different elements at different stages in the program's evaluation. But some Scheme implementations also have immutable vectors. -The other notion in Scheme for expressing longer *n*-tuples is what I'll call the possibly-improper list, or *imp*. (This isn't standard Scheme terminology. I think it's conceptually cleaner to start here and work your way torward the standard Scheme ways of talking.) I won't say yet how you tell Scheme to construct an imp, but they are displayed like this: + +The other notion in Scheme for expressing longer *n*-tuples is what I'll call the possibly-improper list, or **imp**. (This isn't standard Scheme terminology. I think it's conceptually cleaner to start here and work your way toward the standard Scheme ways of talking.) I won't say yet how you tell Scheme to construct an imp, but they are displayed like this: (5 0 . #f) ---- or perhaps with a further single-quote prefix, depending on your configuration. In the special case where the imp is of length 2, these are called "dotted pairs": +--- or perhaps with a further single-quote prefix, depending on your configuration. In the special case where the imp is of length two, these are called **dotted pairs**: (0 . #f) @@ -181,7 +182,7 @@ you won't get the same thing. Instead, you'll get a length-three imp whose first '(5 (- 3 y) . #f) -you won't get an imp whose second element is `0`, even when the variable `y` is bound to the value `3`. Instead, you'll get an imp whose second element is *another imp*, that begins with the symbol `'-` and continues with the number 3. +you won't get an imp whose second element is `0`, even when the variable `y` is bound to the value `3`. Instead, you'll get an imp whose second element is *another imp*, that begins with the symbol `'-` and continues with the number `3`. (By the way, in all of these languages the initial position in a sequence is called position 0, the next position 1, and so on. Some languages start counting from 0, others start counting from 1. Nowadays, most do the former. When we use English ordinals in these web pages, though, we will always use "first" to mean the initial position.) @@ -205,6 +206,8 @@ But if you submit *that, unquoted* expression to Scheme, it gets *evaluated* to that refers to *the imp itself*, rather than to the result of evaluating it in the way just described. Now you see why we call this "quotation", and use the symbol we do for it. You might also see the relation between the *symbol* `'y` (or just `y` when it occurs embedded in the quoted imp above, or even more deeply embedded, as in `'(5 (- 3 y) . #f)`) and the *variable* `y`. For Scheme, symbols just *are* variables, only not yet evaluated. That's why we write the symbol with an initial single-quote. + + The major difference in Scheme between vectors and imps is that imps have this special relation to Scheme's own syntax. Scheme treats its code as itself being complex imps, not as being complex vectors. Another difference is that under the hood, the computer implements vectors and imps differently. Vectors store all of their elements in a contiguous block of memory (an "array"), whereas imps may store them scattered all over the place (as a "linked list"). On early computing hardware, the latter was often-times more useful; on contemporary hardware, it's much less so. But neither vectors nor imps are closer models of (heavyweight) tuples in Kapulet, OCaml, and Haskell than the other. They can both contain type-heterogenous elements, including other vectors and/or imps. Like vectors, but unlike the structures in other languages, imps in Scheme are by default mutable. But many implementations also offer immutable imps. Okay, that's the story you should first get your head around about longer Scheme containers. We'll talk about shorter containers, including unit(s), in a moment. But I've used idiosyncratic language in talking about these "imp"s, and I've suppressed some complications, which we should now consider. @@ -225,7 +228,7 @@ We asked before how you specify an imp. We've seen one notation, using an initia returns the imp whose first member is the symbol `'-` (if you left the quote off, you would instead get the function that this symbol is bound to), whose second member is the number 3, whose third value is whatever value the symbol/variable `y` is bound to, and whose final value is the empty list. -What if you want to instead build an *improper* imp, such as `(5 0 . y)`, again using the value `y` is bound to rather than the quoted symbol? There is no form in *standard* Scheme to do this *directly*. But many Scheme *implementations* do have a special function for it. Racket calls it `list*`, so if you write in Racket: +What if you want to instead build an *improper* imp, such as `(5 0 . y)`, again using the value `y` is bound to rather than the quoted symbol? There is no function in *standard* Scheme to do this *directly*. But many Scheme *implementations* do have a special function for it. Racket calls it `list*`, so if you write in Racket: (let* ((y #f)) (list* 5 0 y)) @@ -244,7 +247,7 @@ Okay, all of that was just us getting clear about Scheme's *longer* containers, Scheme does have vectors of length one: you can write `(vector 5)` or `#(5)`. And that will be distinct from the number `5`. Kapulet is similar: we could have a heavyweight one-tuple, perhaps `Single 5` (or `Single (5)`, the parentheses make no difference when they contain exactly one syntactic atom), that would be distinct from the number `5`. But there is no difference among lightweight tuples. In Kapulet, `(5)` and `5` would just be notational variants for the number. Although OCaml and Haskell for the most part have tuples corresponding to Kapulet's heavyweight tuples, in this case they do not. Some subtleties about their type systems aside, they don't have any native one-tuple `(5)` that's distinct from mere `5`. -Scheme doesn't have any *imps* of length one, because it builds its imps out of dotted pairs, so they can't get any smaller than length two. +Scheme doesn't have any *imps* of length one, because it builds its imps out of dotted pairs, so they can't get any smaller than length two. (Some Schemes do have a separate notion of a *box*, which is isomorphic to vectors of length one.) Now how about unit(s)? Here again, Scheme has vectors of that length: you can write `(vector)` or `#()`. And that will in many ways correspond to the heavyweight Kapulet length-zero tuple `Unit ()`. Here too, there can be no imps that are this short. @@ -261,14 +264,14 @@ your Scheme interpreter will probably just not show any result, not even a blank (display x) -it might have said `#` or `#`. +it might have shown `#` or `#`. Even if your Scheme implementation lacks the `void` function, though --- as the official standard permits it to do --- you can still generate the special *void* value in other ways. One example is if a `cond` expression has no `else` clause, but none of the clauses that *are* supplied succeed. So this will also generate the special *void* value: (cond (#f 'impossible)) -Ok. Let's pull this all together. Scheme has two heavyweight values corresponding to Kapulet's `Unit ()`, namely its length-one vector (expressible as `(vector)` or `#()`) and its special *void* value (expressible in various ways). It has two heavyweight values corresponding to Kapulet's `Triple (0, 5, #f)`, namely a length-three vector and a length-three imp. Corresponding to the *lightweight* Kapulet tuples or multi-values are the Scheme idioms: +Ok. Let's pull this all together. Scheme has two heavyweight values corresponding to Kapulet's `Unit ()`, namely its length-zero vector (expressible as `(vector)` or `#()`) and its special *void* value (expressible in various ways). It has two heavyweight values corresponding to Kapulet's `Triple (0, 5, #f)`, namely a length-three vector and a length-three imp. Corresponding to the *lightweight* Kapulet tuples or multi-values are the Scheme idioms: ; when calling Scheme functions (f 0 5 #f) @@ -288,10 +291,88 @@ Using *unit* as a parameter in a data structure As I mentioned in class, you might sometimes want to use *unit* as a parameter to some existing data structure. For instance, sequences or lists are data structures that have one variant `[]` with no parameters, and a second variant with both a head parameter and a tail parameter that has to be another list. What is the head parameter? Well, in some cases it might be a Number, or a Boolean, or another atomic symbol. Or even a function. But in some cases we might not care about the specific identity of the head. We might only care about the list structure. (For lists, all there is to their structure is their *length*. For other structures, their structure might be more complex and encode more information.) We *could* just make a list of Numbers, and ignore the identity of the particular Numbers used. But it's ugly to make arbitrary choices about which Numbers to use, when we really don't care. It can also mislead readers of our code about how the code works. If we want to specify clearly that we don't care about the identity of the head element, we could instead make it a list of *Units*, where there is only ever one choice for which instance of that type to use. -If the notion so expressed is important enough, we might give it its *own*, dedicated data structure, that just left out the head parameter. Informationally, it's all the same whether you omit some parameter or include it but offer only one choice for what it can be. Indeed I suggested that this is a helpful way to think of Numbers as compared to Lists. +If the notion so expressed is important enough, we might give it its *own*, dedicated data structure, that just left out the head parameter. Informationally, it's all the same whether you omit some parameter or include it but offer only one choice for what it can be. Indeed I suggested that this is a helpful way to think of Numbers as compared to Lists. (In Chapter 6 of *The Little Schemer*, they also briefly explore representing the number `4` as the list `'(() () () ())`.) -But sometimes the more general data structure you're working with will be well-developed, and have lots of code already built up around it, prepared to handle parameters of many types. And the special case where you don't care about the identity of the parameter might be more limited purpose, that's easier to just piggy-back on the more general notion than to write separate code for. In these cases *units* can be a useful choice for the type of some parameter, precisely because they are uninformative. +But sometimes the more general data structure you're working with will be well-developed, and have lots of code already built up around it, prepared to handle parameters of many types. And the special case where you don't care about the identity of the parameter might be more limited purpose, that's easier to just piggy-back on the more general notion than to write separate code for. In these cases *Units* can be a useful choice for the type of some parameter, precisely because they are uninformative. (For these purposes you'd want to use heavyweight units, like Kapulet's `Unit ()` or Haskell and OCaml's `()`. For the remaining jobs to be discussed below, however, arguably *either* of Kapulet's heavyweight or lightweight units could be deployed.) +Returning *unit* as a result +============================ + +Some functions return *Units* as their results. How could that ever be useful? Since there's only ever one result, it seems like the function would have to be a constant function, from whatever arguments it was willing to accept, to that result. Now sometimes constant functions might be useful; for example, you might have an operation that expected some predicate to apply to numbers, and in a particular case you might want every number to count as passing, so you could supply the constant predicate from numbers to truth. But here the constant function's usefulness depends on the possibility of your choosing it rather than some other function with the same result type. Even if you limited yourself only to *constant* functions to Boolean results, you still have two to choose from, and that choice can encode some information. When we turn to constant functions from some arguments into the *Unit* type, on the other hand, there is only one result that any such function could constantly return. What would be the point? + +The point emerges when we have functions that do more than merely *evaluate* other functions and relations on their arguments. Some functions additionally perform *side*-effects, like vocalizing (or printing) their arguments, or changing what the first element in some mutable list value is, or so on. A function can perform side-effects and *also* return a useful value. For example, we might have a "noisy-successor" function that vocalizes the value of its number argument, and evaluates to that argument's successor. So if I ask the computer to evaluate: + + [noisy_succ 3, 5] + +or: + + [noisy_succ (1 + 2), 5] + +The computer will vocalize the word "three", but return the value `4`, to be used in building up a potentially more complex value --- in these cases the length-two sequence `[4, 5]`. + +For some functions, on the other hand, *all we care about* is the side-effect. There may be no informative result to return. Then we're in a situation like we were in the previous section. We *could* return, say, a Number result, chosen arbitrarily. Or a random Boolean. But this is ugly and can mislead readers about what's going on. The cleanest thing to do is to signal explicitly that the result value is not informative. For this *Units* are perfect. + +We haven't yet started to work with any functions with side-effects, but we will later. You may also be familiar with some of these ideas from other contexts, or other programming languages. + + +Supplying *unit* as an argument +=============================== + +Finally, in some cases we have functions that are applied to *Units* as arguments. How can that be useful? What's the difference between applying a function to an uninformative argument and just the original, unapplied function? + +As Chris observed in seminar, we do make such distinctions in natural language. We distinguish between the English *predicate* "is raining" and the *sentence* "*It* is raining." Only the latter can have a truth-value (in a particular context of utterance, that supplies a time and a place). Only the former still has syntactic positions left unsaturated, and can be coordinated with other, similar predicates ("is raining *and won't get warmer*"). Yet that initial "It" is semantically uninformative. It doesn't refer to anything. All it does is provide empty filler. It seems like its sole function is to mark the difference between the predicate and the sentence. + +This is indeed the role *unit* has when it's the argument a function expects or receives. Now, you ask: why would it be *useful* to distinguish between the unapplied and the applied function. If one Kapulet programmer writes: + + let + g match lambda (). 1 + 3; + f match lambda (thunk, y). thunk () * y + in f (g, 5) + +and another writes merely: + + let + g' match 1 + 3; + f' match lambda (x, y). x * y + in f' (g', 5) + +what advantage can the first have possibly achieved? Why bind `g` to such a *function* --- these functions that take `()` arguments are known as **thunks** --- which later gets applied, but to an uninformative argument, rather than just calculating the function's result in the first place, and binding a variable that *that*? + +The main usefulness of this is again when we're dealing with functions that have side-effects. If `g` were bound instead to `lambda (). noisy_succ 3`, we might want to control when the body of that function gets computed. We might be in a position to build the function at one point in the program, but only want it to be computed at some distant point, related to where we are now by a complex path. Also, we might want to control *how often* the body gets computed. If for example, we say: + + let + g match lambda (). noisy_succ 3; + f match lambda (thunk, y). thunk () * thunk () * thunk () * y + in f (g, 5) + +that will result in the computer vocalizing "three" three times, and returning `320`. Whereas if we say: + + let + g' match noisy_succ 3; + f' match lambda (x, y). x * x * x * y + in f' (g', 5) + +that will result in the computer vocalizing "three" only once, and returning the same result. (I assume here our language has what we called "strict" or "eager" evaluation order, where a function's arguments are evaluated *before* being substituted into the body of the function. Most programming languages work like this; but Haskell does not do so by default. With the Lambda Calculus, it may or may not; you have to decide.) + +Here again, we haven't started to work with any functions with side-effects, so you have to imagine ahead ways in which this could be useful. + +But in this case, we *have* also encountered this very week *another* way in which delaying the evaluation of a function in this way could be useful. Some expressions just won't evaluate to any value. In the Lambda Calculus, we had the example of ω ω, that is, `(\x. x x) (\x. x x)`. And also some even scarier terms. We can write these in Kapulet, or we can write other scary terms using `letrec`: + + letrec + blackhole match lambda (). blackhole (); + fst match lambda (x, y). x + in fst (5, blackhole) + +If `blackhole` ever gets applied to its `()` argument, computing the result will require re-applying `blackhole` to that same argument, which will require ... and the reduction or computation will never terminate. Some of the vocabulary people use for such expressions is that they involve "infinite loops" or "non-termination" or that they "diverge" or that their "meaning is bottom" (written , as we sometimes use in logic to represent a constant formula that is always false). "Bottom" is a meaning we might assign these terms, but don't say that this is their *value*. Such terms don't have any value. (Sometimes people talk sloppily, and we might even do it ourselves. But the best way to talk here is to say that expressions like `blackhole ()` *don't have any values*, or in other words *don't evaluate to anything*, though our semantics may assign them a *meaning* or denotation. Interestingly, you can't in general assign all non-terminating expressions the same meaning. See ___ for discussion.) + +But in the complex expression above, we never do apply `blackhole` to an argument, so no harm comes about. It makes no difference that we were evaluating `fst (5, blackhole)` rather than `snd (blackhole, 5)` (or even `snd (5, blackhole)`). In none of the cases do we ever request the result of computing `blackhole`'s body. So everything is ok. + +This is like how in the Lambda Calculus it can be alright to say: + +K I (ω ω) + +--- that is, assuming the lambda term is being evaluated in "normal" or "lazy" order, so that we reduce the leftmost, outermost redex `K I (...)` before we reduce the argument redex ω ω. Since `K I (...)` discards its second argument, the non-terminating computation of ω ω (that is, the result of self-applying self-application) is never demanded. +