• Aucun résultat trouvé

Laurent Dami

6.2 A Lambda Calculus with Named Parameters

It is well known that names in the standard lambda calculus are only useful to express a relationship between binding occurrences and applied occurrences of variables. Once that relationship is established, i.e. with bound variables, names can be replaced by other names through α substitution, or can even be removed altogether: in [8] de Bruijn pro-posed a modified lambda calculus in which variables are simply denoted by indices. A de Bruijn index is a non-negative integer expressing the distance between an applied occur-rence of a variable and the abstraction level to which it refers. For example, the not func-tion, written

Not = λb.λt.λf.b f t

in the standard calculus, becomes

Not = λλλ 2 0 1

in de Bruijn notation (here we start indices with 0, while some authors start with 1; the dif-ference is not significant). There is a straightforward translation from usual lambda ex-pressions to their de Bruijn version. The de Bruijn notation provides a canonical representation: all α-equivalent lambda expressions have the same translation. Further-more, the well-known problem of name capture is avoided. Both in standard and de Bruijn calculi, each abstraction level (each ‘λ’) introduces exactly one variable. Our proposal is to allow several variables at the same abstraction level. To do so, de Bruijn indices are re-tained, but in addition names are used to distinguish between different variables at the same level. This section defines the calculus; the next section shows that this extension provides support for plug-compatibility.

6.2.1 Abstract (Low-level) Syntax

Figure 6.1 presents the abstract syntax of λN. The language is built over a (finite) set of names.

An abstraction corresponds to the traditional notion of abstraction. Like in the de Bruijn lambda calculus, abstractions need not introduce names for their parameters: the connection between variables and their corresponding abstraction level directly comes from the indices associated with variables (see below).

A variable is a name together with a de Bruijn index. This means that an abstraction can have several parameters, all with the same index, which are distinguished by their name.

The index indicates the abstraction level (which ‘λ’) a variable is referring to: an index of 0 refers to the closest abstraction, and higher numbers refer to farther abstraction levels.

A bind operation partly corresponds to the usual notion of application. However, since an abstraction may have several parameters, it is necessary to specify which of them is bound in the expression. Therefore the construct a(x->b) means: “bind b to the parameter

x, y, z ∈ Names i, j ∈ Nat a, b, ... ∈ Terms

a := λ a abstraction

| (x, i) variable

| a(xb) bind operation

| a! close operation

Figure 6.1 Abstract syntax.

with name x in a”, or, expressed differently: “substitute b for every occurrence of (x, 0) in a (modulo index renumbering, as defined below)”. The parameters of an abstraction may be bound separately, and in any order.

A close operation closes a sequence of bindings, and removes an abstraction level (re-moves one ‘λ’).

Notions of parameters, free and bound variables are as in the de Bruijn calculus; a for-mal definition is given in figure 6.2.

6.2.2 Reduction Rules

In the de Bruijn calculus, β-reduction involves some renumbering of indices: whenever the number of ‘λ’s above a subterm changes, its free variables have to be adapted in con-sequence. One way to express it is

(λa) b →β0[a [0 := ↑0[b]]

where ‘↑’ (lift) is an operation incrementing all free variables by 1, ‘↓’ (unlift) is the re-verse operation, and a[i := b] is the substitution of b for all occurrences of i in a (again modulo index renumbering).

The reduction rules for λN, given in figure 6.3, are very similar, since they also involve index manipulation operations. There are two kinds of reductions, called bind reduction (β) and close reduction (γ). Basically, the operations performed by β-reduction in the standard lambda calculus have been split in two: binding reductions substitute values for variables, and close reductions “remove the lambda” and unlift the result, i.e. they remove an abstraction level. The definitions for lifting and substitution operations are given in fig-ure 6.4

FVk((x, i)) = if i =k then {(x, i)} else {}

FVk(λa) = {(x, i) | (x, i+1) ∈ FVk+1(a)}

FVk(a(xb)) = FVk(a) ∪ FVk(b)

FVk(a!) = FVk(a)

FV(a) = ∪

k ≥ 0 FVk(a) parameters(λa) = FV0(a)

a closed ⇔ FV(a) = {}

Figure 6.2 Free and bound variables .

Careful readers will have noticed that in λN we may need to unlift a 0 index, a situation which never occurs in the de Bruijn calculus. Consider de Bruijn’s β-reduction rule above:

all 0 indices are substituted in a, so the expression passed to ‘↓’ contains no 0 index. By contrast, the λN expression (λ (x, 0))! reduces to ↓0[ (x, 0) ], which intuitively corresponds to an error (we are trying to access a parameter that has not been bound). As a matter of fact, in such situations the definition of ‘↓’ yields err, a specific term representing errors.

This will be discussed in detail in section 6.4; for the time being it suffices to know that err

is not an additional syntactic construct, but rather is defined as a usual term in the lan-guage, with the property that further binding or close operations on err yield err again.

A binding reduction can never introduce new parameters in an abstraction, because the term passed in the substitution is lifted. Therefore if several successive bindings are done, the final result does not depend on the order of the substitutions. This amounts to say that bindings are commutative, i.e. expressions of the form

a(x→b)(y→c) and a(y→c)(x→b)

derive to the same thing, provided that x and y are different names. If x and y are the same (λa)(xb) →β λ(a[(x, 0) : = ↑0[b]])

(λa)! →γ0[a]

Figure 6.3 Reduction rules.

Lifting/Unlifting

k[(x, i)] = if (i < k) then (x, i) else (x, i+1)

k[(x, i)] = if (i < k) then (x, i) else if (i=k) then err else (x, i-1)

k [λa] = λ (k+1 [a])

k [a(xb)] = k [a](xk [b])

k [a!] = (k [a])! where ‘’ is either ‘’ or ‘↑’

err =def E(xE)! where E = λλ (x, 1)(x(x, 1))!

Substitution

(y, j)[(x, i) := b] = if ((x, i) = (y,j)) then b else (y, j) (λa) [(x, i) := b] = λ(a[(x, i+1) := ↑0[b]])

(a(yc)) [(x, i) := b] = (a[(x, i) := b])(yc[(x, i) := b]) (a!)[(x, i) := b] = (a[(x, i) := b])!

Figure 6.4 Lifting and substitution operations.

name, all references to that name are substituted in the first binding, so the second binding is just ignored, and those bindings are not commutative.

6.2.3 Reduction Example

For illustrating the rules, we use again the expression Not(bTrue)!. The derivation was given in an informal way in the introduction, using high-level syntax (without indices).

Here, the low-level syntax is used; at each step, the lambda and the bind or close operation involved in the next reduction step are underlined.

1 (λλ (arg, 1) (true(false, 0))(false(true, 0))!)(argλ(true, 0))!

2 (λλ (λ(true, 0))(true(false, 0))(false(true, 0))!) ! 3 λ (λ(true, 0))(true(false, 0))(false(true, 0))!

4 λ (λ(false, 1))(false(true, 0))!

5 λ (λ(false, 1)) ! 6 λ (false, 0)

The final result is False. Notice at line 4 that the binding of false simply gets eliminated:

this is because the abstraction (λ(false, 1)) has no parameter called false; it indeed uses a variable with that name, but since the index is not 0 this is a free variable, not a parameter.

At some intermediate stages (e.g. at line 2) several reductions could occur; the se-quence shown here corresponds to normal-order reduction (choosing leftmost outermost redex first). It is therefore legitimate to ask whether a different reduction sequence would yield the same result (whether the language is confluent). The answer is yes, and has been established in [13]. So, as in the standard lambda calculus, results are independent from the reduction sequences through which they were obtained; furthermore, if an expression does have a result, then the normal-order reduction strategy is guaranteed to yield that re-sult (i.e. not to diverge).

Notice that if we “forget” to supply an argument to Not before applying a close opera-tion, as in Not!, we have the reduction

(λλ (arg, 1) (true(false, 0))(false(true, 0))!) !

λ err (true(false, 0))(false(true, 0))!

λ err (false(true, 0))!

λ err ! λ err

which is equivalent to err, i.e. an error is produced.

6.2.4 Higher-level Syntax

Indices were necessary for defining the calculus, but are difficult to read. In order to work practically with the calculus, we will use a higher-level syntax, given in figure 6.5, in which the indices need not be explicitly written. There is a straightforward translation T

from this syntax into the original syntax, which is formally defined in figure 6.6. In this

new notation, the parameters of an abstraction are declared as a list of names in parenthe-sis. A variable is written simply as a name: the index is recovered by looking for the closest abstraction which declares the same name. In case the same name is used at several ab-straction levels, and one wants to override the default variable matching scheme, the name of the variable can be preceded by a collection of backslashes. This tells the translation function to start looking for a declaration, not at the next abstraction level, but one or sev-eral levels higher (according to the number of backslashes). The parameter list following a lambda can be empty, as in

λ() Not(argTrue)!

This is like a closure, i.e. a function that needs no arguments but is not evaluated yet (as-suming a lazy interpretation as in section 6.4.1). Forcing evaluation is then done with the

‘!’ operator.

The translation T from this syntax into the original syntax is like translating the standard lambda calculus into de Bruijn notation (see [12]). The first argument to the translation function is a set of currently declared variables; at each abstraction level this set is updat-ed. The translation is defined for closed terms by taking the initially empty set of variables.

Variables which are not declared at any level are translated into an error by the matchVar function. As an example of a translation, consider the expression

v := x simple variable

| \v “outer” variable

a := λ(x1 … xn) a abstraction

| v variable

| a(xb) bind operation

| a! close operation

Figure 6.5 Higher-level syntax.

TV [λ(x1 … xn) a] = λ (TV' [a])

where V' ={(x, i+1) | (x, i) ∈ V} ∪ {(x1, 0), ..., (xn, 0)}

TV [\...\x ] = matchVar V (x, i) where i is the number of ‘\’

TV [a(xb)] = TV [a](xTV [b])

TV [a!] = (TV [a])!

matchVar V (x, i) = let J={j | (x, j) ∈ V, j ≥ i} in if (J = {}) then err

else (x, min(J))

Figure 6.6 Translation function.

λ(x y) λ(x z) x + y + z + \x + \y + \z + \\x

(assuming that infix addition is part of the language). After crossing the two abstraction levels, the set V of declared variables is

V={(x, 1), (y, 1), (x, 0), (z, 0)}

and therefore the translation is

λλ (x, 0) + (y, 1) + (z, 0) + (x, 1) + (y, 1) + err + err

This shows how the backslash can be used to distinguish between parameters with the same name, but at different levels. Notice that x and \x are different variables, while both y and \y are translated into (y, 1), because there is no y parameter at the inner abstraction level. Furthermore, both \z and \\x are translated into err, because no corresponding varia-ble declaration can be found.