Formal Software Verification

3.4. Polymorphism and Implicit Arguments🔗

A definition can take a type as an argument. The function below appends two lists over any type α, given explicitly at each call, and Lean's _ asks the elaborator to infer it. The elaborator is the stage of Lean that turns the text we write into a term of the core language, and inference is part of its work, together with the resolution of type class instances and the execution of tactics. Figure 1.1 places it among the other components. Writing _ therefore states that the argument is determined by the rest of the call, and the elaborator recovers it by unification.

def append (α : Type) : List α List α List α | List.nil, ys => ys | List.cons x xs, ys => List.cons x (append α xs ys) [3, 1, 4, 1, 5]#eval append [3, 1] [4, 1, 5]

Curly braces make the type argument implicit, inferred at every use. The @ prefix restores the explicit form when needed.

def appendImplicit {α : Type} : List α List α List α | List.nil, ys => ys | List.cons x xs, ys => List.cons x (appendImplicit xs ys) [3, 1, 4, 1, 5]#eval appendImplicit [3, 1] [4, 1, 5] @appendImplicit : {α : Type} List α List α List α#check @appendImplicit

Lean's list notation writes List.nil as [], List.cons x xs as x :: xs, and chains of cons as [x₁, x₂, x₃]. With it, the definition reads like its own specification.

def appendPretty {α : Type} : List α List α List α | [], ys => ys | x :: xs, ys => x :: appendPretty xs ys

Reversal follows the same shape, appending the head at the far end.

def reverse {α : Type} : List α List α | [] => [] | x :: xs => appendPretty (reverse xs) [x]

3.4.1. Examples🔗

The examples below compare explicit and implicit type arguments and define polymorphic functions over lists and pairs.

Example 1. With an explicit type argument, the type appears in the signature as an ordinary argument.

append : (α : Type) List α List α List α#check @append
append : (α : Type)  List α  List α  List α

Example 2. Curly braces mark the argument as implicit, and @ displays it.

@appendPretty : {α : Type} List α List α List α#check @appendPretty
@appendPretty : {α : Type}  List α  List α  List α

Example 3. At a call, the implicit argument comes from the type of the lists.

[1, 2, 3]#eval appendPretty [1, 2] [3]
[1, 2, 3]

Example 4. The same definition serves another type without change.

["a", "b"]#eval appendImplicit ["a"] ["b"]
["a", "b"]

Example 5. The @ prefix restores the explicit form, useful when inference has nothing to work with.

[1, 2]#eval @appendImplicit [1] [2]
[1, 2]

Example 6. The identity function is polymorphic and returns its argument unchanged.

def idPoly {α : Type} (x : α) : α := x @idPoly : {α : Type} α α#check @idPoly
@idPoly : {α : Type}  α  α

Example 7. Building a one-element list works at every type.

def singletonList {α : Type} (x : α) : List α := [x] [5]#eval singletonList 5
[5]

Example 8. A definition can take two type arguments. Swapping the components of a pair exchanges them.

def swapPair {α β : Type} : α × β β × α | (x, y) => (y, x) ("x", 1)#eval swapPair (1, "x")
("x", 1)

Example 9. The length of a list ignores the elements, so the type argument never appears in the result.

def lengthPoly {α : Type} : List α | [] => 0 | _ :: xs => lengthPoly xs + 1 3#eval lengthPoly ["a", "b", "c"]
3

Example 10. An empty list carries no element to infer from, and a type ascription fixes the implicit argument.

[] : List #check ([] : List )
[] : List