Interactive Theorem Proving and Program Development
ion makes it possible to build non-recursive functions directly inside Calculus of Constructions terms, without giving a name to them, but for recursive functions the Fixpoint command always gives a name to the defined function. It mixes the two operations: first the description of a recursive function, second the definition of a constant having this function as value. With the fix construct, we can have only the first operation; in this sense, it is similar to the abstraction construct. Here is the syntax for this construct: As with the Fixpoint command, the {struct ai} is not mandatory if p = l. In the particular case where one defines only one recursive function the two occurrences of the identifier f must coincide. This identifier is used to denote the recursive function being defined but it can only be used inside the expression expr. The similarity between Fixpoint and fix makes it easier to understand the need for the various parts of this construct. For instance, the mul t2 function could also have been declared in the following manner: Definition mult2' : nat~nat := fix f (n:nat) : nat .= match n with 0 =} 0 I S p =} S (S (f p)) end. Here we have willingly changed the name given to the recursive function inside the fix construct to underline the fact that f is bound only inside the construct. Thus, this identifier has no relation with the name under which the function will be known. 6.4 Polymorphic Types 175 6.4 Polymorphic Types Among the operations that one can perform on binary trees carrying integer values, many rely only on the tree structure, but are independent of the fact that the values are integer values. For instance, we can compute the size or the height of a tree without looking at the values. It is sensible to define a general type of tree, in which the type of elements is left as a parameter, and use instances of this general type according to the needs of our algorithms. This is similar to the polymorphic data structures available in conventional functional languages, or the generic data structures of Ada. We describe this notion of polymorphism on lists, pairs, etc. 6.4.1 Polymorphic Lists The Coq system provides a theory of polymorphic lists in the package List. Require Import List. Print list. Inductive list (A : Set) : Set := nil : list A / cons : A -+ list A -+ list A For nil: Argument A is implicit For cons: Argument A is implicit For list: Argument scope is [type_scope] For nil: Argument scope is [type_scope] For cons: Argument scopes are [type_scope __ ] The Coq system provides a notation for lists, so that the expression" cons a l" is actually denoted "a: : l." We see here that the inductive type being defined does not occur as a simple identifier, but as a dependent type with one argument. The value of this argument is always A, the parameter of the definition, as given in the first line of the definition. This definition behaves as if we were actually defining a whole family of inductive types, indexed over the sort Set. This illustrates the construction of higher-order types that we saw in Chap. 4. There may be several parameters in an inductive definition. When parameters are provided, they must appear at every use of the type being defined. Everything happens as if the inductive type had been declared in a section, with a context where A is bound as a variable. Thus, the definition above is equivalent to a definition of the following form: Section define_lists. Variable A : Set. Inductive list' : Set := I nil' : list' I cons' : A -+ list' -+ list'. End define_lists. 176 6 Inductive Data Types This analogy between polymorphic inductive types and simple inductive types defined inside a section is helpful to understand the form of the induction principle. Let us first study the type of the induction principle as it would have been constructed inside the section: list'_indO : 'v'P : list' --+Prop, P nil' --+ ('v'(x:A)(l:list'), P 1 --+ P (cons' x 1»--+ 'v'x:list', P x. When the section is closed, the variable A is discharged, the type list' must be abstracted over A, the constructors, too, and the induction principle must take these changes into account: Check list'. list' : Set--+Set Check nil'. nil' : 'v' A:Set, list' A Check cons'. cons' : 'v' A:Set, A --+ list' A --+ list' A Check list'_ind. list' ind: 'v' (A:Set)(P:list' A --+ Prop), P (nil' A) --+{V (a:A)(l:list' A), P l--+ P (cons' A a l)) --+ 'v' l:list' A, P l From a practical point of view, an important characteristic of parametric inductive definitions is that the universal quantification appears before the universal quantification over the property that is the object of the proof by induction. This characteristic will be important in comparison with the inductive principles for inductive definitions of variably dependent types (see Sect. 6.5.2) Recursive functions and pattern matching on polymorphic types can be performed in the same manner as for the inductive types of the previous sections. However, there is an important difference; the parameters must not appear in the left-hand side of pattern matching clauses. For instance, the function to concatenate polymorphic lists is defined by an expression of this form: Fixpoint app (A:Set) (1 m:list A){struct I} : list A := match 1 with I nil :::} m I cons a 11 :::} cons a (app A 11 m) end. In this pattern matching construct, nil appears in the left-hand side of its clause without its Set argument. The same occurs for the cons constructor, 6.4 Polymorphic Types 177 even though cons normally has three arguments; the pattern only has two. The reasoh for removing the parameter arguments from the constructors is that these parameters cannot be bound in the pattern. The type A for the values is already fixed because an expression of type "list A" is being analyzed by the pattern matching construct. In the right-hand side of the second clause, cons also appears with two arguments, but this is because the function is defined with the first argument being implicit (see Sect. 4.2.3.1). Use of implicit arguments for functions manipulating polymorphic types is frequent. For instance, the function app also has its first argument as an implicit argument. For this function, the Coq system also provides an infix notation, where "app h h" is actually denoted "h++h." Exercise 6.34 Build a polymorphic function that takes a list as argument and returns a list containing the first two elements when they exist. Exercise 6.35 Build a function that takes a natural number, n, and a list as arguments and returns the list containing the first n elements of the list when they exist. Exercise 6.36 Build a function that takes a list of integers as argument and returns the sum of these numbers. Exercise 6.37 Build a function that takes a natural number n as argument and builds a list containing n occurrences of the number one. Exercise 6.38 Build a function that takes a number n and returns the list containing the integers from 1 to n, in this order. 6.4.2 The option Type Polymorphic types need not be truly recursive. A frequent example is the option type that is well-adapted to describe a large class of partial functions. This type is also present in conventional functional programming languages. Its inductive definition has the following form: Print option. Inductive option (A:Set) : Set := Some: A-+option A / None: option A For Some: Argument A is implicit For None: Argument A is implicit For option: Argument scope is [type_scope] For Some: Argument scopes are [type_scope _] For None: Argument scope is [type_scope] 178 6 Inductive Data Types When we need to define a function that is not total from a type A to a type B, it is often possible to describe it as a total function from A to "option B," with the convention that the value "None" is the result when the function is not defined and the value is "Some y" when the function would have been defined with value y. For instance, the Coq library contains a pred function of type nat---+nat that maps any natural number to its predecessor (when it exists) and maps zero to itself. A partial function could have been defined that does not have a value for the number zero. The definition would have been as follows: Definition pred_option (n:nat) : option nat := match n with 0 => None I S P => Some pend. To use a value of the option type, a case analysis is necessary, to express explicitly how the computation proceeds when no true value is given. For instance, the function that returns the predecessor's predecessor can be defined as follows: Definition pred2_option (n:nat) : option nat := match pred_option n with I None => None I Some p => pred_option p end. As a second example, we can consider the function that returns the nth element of a list. The Coq library provides a function called nth for this requirement but that function takes an extra argument which is used for the result when the list has less than n elements. An alternative could be to define a function whose result belongs in the option type. This function can be defined using simultaneous pattern matching on the number and the list. Both arguments decrease at each recursive call, so that the principal recursion argument could be either of them. Here is one of the two possible versions: Fixpoint nth_option (A:Set) (n:nat) (l:list A){struct l} : option A := match n, 1 with I 0, cons a tl => Some a I S p, cons a tl => nth_option A p tl I n, nil => None end. Exercise 6.39 Define the other variant "nth_option'." The arguments are given in the same order, but the principal argument is the number n. Prove that both functions always give the same result when applied on the same input. Exercise 6.40 * Prove V(A:Set)(n:nat)(l:list A), nth_option A n 1 = None ---+ length 1 < n. 6.4 Polymorphic Types 179 Exercise 6.41 * Define a function that maps a type A in sort Set, a function f of type A---+booL, and a list L to the first element :z: in L such that "f x" is true. 6.4.3 The Type of Pairs Pairs prov