Fall 2000, CSE 520: Lecture 6

Representing partial functions faithfully

The encoding of a partial function f that we have seen in previous lectures satisfies the following property:
For all n1, n2, ... nk,     if   f(n1, n2, ..., nk)   is defined,   then     [f] [n1] [n2] ... [nk] = [f(n1, n2, ..., nk)]
We don't say anything, however, of what happens if   f(n1, n2, ... nk)   is undefined. In general, it is not guarranteed that the encoding is undefined as well: there may exist n such that   [f] [n1] [n2] ... [nk] = [n]. We will see later why this is possible.

It is natural to ask ourselves whether such encoding is good enough. It would be better, of course, to have an encoding that, in all cases in which the original function is undefined, gives as result some particular lambda term which we take as representation of "undefined". However, this is not possible: One of the basic results of the Theory of Computability says that it is not possible to give (in any system) an encoding of all computable functions as total functions.

So, there are only two alternatives: either we don't care of what our encoding gives in case the original function is undefined, or we enforce our encoding to be "undefined" as well, in the sense that we ensure that it will not lambda-convert to (the representation of) any number.

So far, we have followed the first approach. The second one, however, may be preferable: One can argue that it is better to have "no answer" rather than "a wrong answer".

In order to encode functions faithfully also in the cases of undefined result, let us first understand what are the causes of the mismatch in our previous encoding. There is essentially only one cause, and it is related to the dicotomy call-by-value / call-by-name in the definition of functional composition.

Call by value and call by name

This terminology refers to the way function calls are evaluated. Let us explain the difference with an example. Consider the following functions defined in some programming language:
f(x) = 0     for all x
g(x) = undefined     for all x
What is the result of the call f(g(1))? It depends, of course, on the way the parameter is evaluated. We have two choices:
  1. We fist evaluate the parameter g(1), and then we apply the definition of f. This strategy is called  call by value. In our example, the evaluation of g(1) is undefined and therefore the result of f(g(1)) is undefined as well.
  2. We apply the definition of f, and we evaluate the parameter g(1) only if necessary. This strategy is called  call by name. In our example, the definition of f does not require the evaluation of g(1) and therefore the result of f(g(1)) is 0.

Functional composition in Gödel's system of Recursive functions and in the lambda calculus

In the system of Gödel, functional composition is defined as it is usually defined in mathematics, namely it follows the call-by-value principle. Thus, if f and g are defined as in the example above, then the functional composition (f o g) is undefined on all arguments.

In the lambda calculus, however, the encoding of (f o g) (for the f and g defined above) is:

[f o g] =def= \x. [f] ([g] x) = \x. (\y. [0]) ([g] x)
hence we have, for instance,
[f o g][1] = (\x. (\y. [0]) ([g] x)) [1] = (\y. [0]) ([g] [1])) = [0]
Note that in the term (\y. [0]) ([g] [1])) there are two beta-conversions that we can apply: one is the top level one, which gives [0] as result, and which corresponds to call-by-name. The other is the internal one, namely the application of [g] to [1], which corresponds to call-by-value. If we keep repeating the internal one only, we may never terminate. Anyway, from the point of view of the equality theory, what counts is that at least one strategy brings to a result.

Simulation of call-by-value in lambda calculus

In order to represent partiality faithfully, we need to simulate the mechanism of call-by-value. This can be done by defining the functional composition in the following way (where we consider for simplicity only the case of unary functions):
[f o g] =def= \x. (([g] x) [true] I I) ([f] ([g] x))
where I = \x.x

It is easy to show that, if [g] [n] = [k], then [f o g] [n] = [f] [k]. In fact (\x. (([g] x) [true] I I) ([f] ([g] x))) [n] = (([g] [n]) [true] I I) ([f] ([g] [n])) = ([k][true] I I) ([f] [k]) = ([true]k I I) ([f] [k]) = I ([f] [k]) = [f] [k].

On the other hand, it is possible to show (even if we won't see the proof here) that, if [g] [n] is "undefined" (in the sense that it does not have a normal form, a concept that we will see later) then [f o g] [n] does not have a normal form either, which means, in particular, that there exists no m such that [f o g] [n] = [m].

The reason, intuitively, is that the term (([g] [n]) [true] I I) ([f] ([g] [n])) can be reduced to a normal form only if we eliminate the top-level application. But this is possible only if we reduce [g] [n] to a normal form. In other words, we are simulating the mechanism of call-by-value by enforcing the evaluation of [g] [n] in order to achieve a normal form for the whole expression.

With the new encoding of functional composition, and the encodings of primitive recursion and mnimalizion as before, we obtain the following stronger property for any recursive function f:

For all n1, n2, ... nk