Structures
Lists
Module MonoList.
Inductive natlist : Type :=
| nat_nil
| nat_cons (n : nat) (l : natlist).
Fixpoint natlist_length (l : natlist) : nat :=
match l with
| nat_nil ⇒ 0
| nat_cons _ l' ⇒ 1 + natlist_length l'
end.
Compute (natlist_length (nat_cons 1 (nat_cons 2 (nat_cons 0 nat_nil)))). (* ==> 3 *)
Inductive boollist : Type :=
| bool_nil
| bool_cons (b : bool) (l : boollist).
Fixpoint boollist_length (l : boollist) : nat :=
match l with
| bool_nil ⇒ 0
| bool_cons _ l' ⇒ 1 + boollist_length l'
end.
Compute (boollist_length (bool_cons true (bool_cons true (bool_cons false bool_nil)))). (* ==> 3 *)
End MonoList.
Inductive natlist : Type :=
| nat_nil
| nat_cons (n : nat) (l : natlist).
Fixpoint natlist_length (l : natlist) : nat :=
match l with
| nat_nil ⇒ 0
| nat_cons _ l' ⇒ 1 + natlist_length l'
end.
Compute (natlist_length (nat_cons 1 (nat_cons 2 (nat_cons 0 nat_nil)))). (* ==> 3 *)
Inductive boollist : Type :=
| bool_nil
| bool_cons (b : bool) (l : boollist).
Fixpoint boollist_length (l : boollist) : nat :=
match l with
| bool_nil ⇒ 0
| bool_cons _ l' ⇒ 1 + boollist_length l'
end.
Compute (boollist_length (bool_cons true (bool_cons true (bool_cons false bool_nil)))). (* ==> 3 *)
End MonoList.
... but this would quickly become tedious, partly because we
have to make up different constructor names for each datatype, but
mostly because we wouldn't get any reuse: if you're computing the
length of a list, it doesn't matter what type the list holds... so
why write the function more than once?
To avoid all this repetition, Coq supports polymorphic
(Greek: "poly-" many, "morphos" form, shape) inductive type
definitions. For example, here is a polymorphic list
datatype.
This is exactly like the definition of natlist above,
except that the nat argument to the cons constructor has been
replaced by an arbitrary type X, a binding for X has been
added to the function header on the first line, and the
occurrences of natlist in the types of the constructors have
been replaced by list X.
What sort of thing is list itself? A good way to think about it
is that the definition of list is a function from Types to
Inductive definitions; or, to put it more concisely, list is a
function from Types to Types. For any particular type X,
the type list X is the Inductively defined set of lists whose
elements are of type X.
Another name for a function that produces Types is "type
constructor": list is a type constructor; if you give it a type
X, then list X will "construct" the type of lists holding
Xs.
The parameter X in the definition of list automatically
becomes a parameter to the constructors nil and cons -- that
is, nil and cons are now polymorphic constructors; when we use
them, we must now provide a first argument that is the type of the
list they are building. For example, nil nat constructs the
empty list of type nat.
Similarly, cons nat adds an element of type nat to a list of
type list nat. Here is an example of forming a list containing
just the natural number 3.
What might the type of nil itself be? We can tell from
Check (nil nat) above that nil takes a Type as input and
produces a list as input.
So you might say that nil : Type → list _. But what's _?
Well, it's whatever Type was provided as nil's first argument!
For nil to refer back to its type argument, we need a new
feature: ∀ types. If e : ∀ (X:T1), T2, then e
takes an X of type T1 and produces a value of type T2.
That is, nil takes a Type -- call it X -- and produces a
value of type list X. The type of the return value depends on
the type given as input.
Similarly, the type of cons from the definition looks like
X → list X → list X, but using this convention to explain the
meaning of X results in the type ∀ X, X → list X → list
X.
(Side note on notation: In .v files, the "forall" quantifier
is spelled out in letters. In the generated HTML files and in the
way various IDEs show .v files, depending on the settings of their
display controls, ∀ is usually typeset as the usual
mathematical "upside down A," though you'll still see the
spelled-out "forall" in a few places. This is just a quirk of
typesetting: there is no difference in meaning.)
Having to supply a type argument for every single use of a
list constructor would be rather burdensome; we will soon see ways
of reducing this annotation burden.
We can now go back and make polymorphic versions of all the
list-processing functions that we wrote before. Here is repeat,
for example:
Fixpoint length (X : Type) (l : list X) : nat :=
match l with
| nil _ ⇒ 0
| cons _ _ l' ⇒ 1 + length X l'
end.
Compute length nat (cons nat 1 (cons nat 2 (cons nat 0 (nil nat)))). (* ==> 3 *)
Compute length bool (cons bool true (cons bool true (cons bool false (nil bool)))). (* ==> 3 *)
Check (length nat).
Check (length base).
Check length.
Fixpoint repeat (X : Type) (x : X) (count : nat) : list X :=
match count with
| 0 ⇒ nil X
| S count' ⇒ cons X x (repeat X x count')
end.
Compute (repeat nat 4 2).
Compute (repeat bool false 1).
Check (repeat bool false).
Check (repeat bool).
Check (repeat day).
Check repeat.
match l with
| nil _ ⇒ 0
| cons _ _ l' ⇒ 1 + length X l'
end.
Compute length nat (cons nat 1 (cons nat 2 (cons nat 0 (nil nat)))). (* ==> 3 *)
Compute length bool (cons bool true (cons bool true (cons bool false (nil bool)))). (* ==> 3 *)
Check (length nat).
Check (length base).
Check length.
Fixpoint repeat (X : Type) (x : X) (count : nat) : list X :=
match count with
| 0 ⇒ nil X
| S count' ⇒ cons X x (repeat X x count')
end.
Compute (repeat nat 4 2).
Compute (repeat bool false 1).
Check (repeat bool false).
Check (repeat bool).
Check (repeat day).
Check repeat.
Type Annotation Inference
Fixpoint repeat' X x count : list X :=
match count with
| 0 ⇒ nil X
| S count' ⇒ cons X x (repeat' X x count')
end.
match count with
| 0 ⇒ nil X
| S count' ⇒ cons X x (repeat' X x count')
end.
Indeed it will. Let's see what type Coq has assigned to repeat':
It has exactly the same type as repeat. Coq was able to
use type inference to deduce what the types of X, x, and
count must be, based on how they are used. For example, since
X is used as an argument to cons, it must be a Type, since
cons expects a Type as its first argument; matching count
with 0 and S means it must be a nat; and so on.
This powerful facility means we don't always have to write
explicit type annotations everywhere, although explicit type
annotations can still be quite useful as documentation and sanity
checks, so we will continue to use them much of the time.
Type Argument Synthesis
repeat' X x count : list X := we can also replace the types with holes
repeat' (X : _) (x : _) (count : _) : list X := to tell Coq to attempt to infer the missing information.
Fixpoint repeat'' X x count : list X :=
match count with
| 0 ⇒ nil _
| S count' ⇒ cons _ x (repeat'' _ x count')
end.
match count with
| 0 ⇒ nil _
| S count' ⇒ cons _ x (repeat'' _ x count')
end.
In this instance, we don't save much by writing _ instead of
X. But in many cases the difference in both keystrokes and
readability is nontrivial. For example, suppose we want to write
down a list containing the numbers 1, 2, and 3. Instead of
this...
...we can use holes to write this:
Implicit Arguments
Now, we don't have to supply type arguments at all:
Alternatively, we can declare an argument to be implicit
when defining the function itself, by surrounding it in curly
braces instead of parens. For example:
Fixpoint repeat''' {X : Type} (x : X) (count : nat) : list X :=
match count with
| 0 ⇒ nil
| S count' ⇒ cons x (repeat''' x count')
end.
match count with
| 0 ⇒ nil
| S count' ⇒ cons x (repeat''' x count')
end.
Let's finish by implementing a few other standard list
functions on our new polymorphic lists.
Fixpoint app {X : Type} (l1 l2 : list X)
: (list X) :=
match l1 with
| nil ⇒ l2
| cons h t ⇒ cons h (app t l2)
end.
Compute (app nil (cons 1 (cons 2 nil))).
Compute (app (cons 2 (cons 7 nil)) nil).
Compute (app (cons 2 (cons 7 nil)) (cons 3 (cons 1 (cons 0 nil)))).
Fixpoint rev {X:Type} (l:list X) : list X :=
match l with
| nil ⇒ nil
| cons h t ⇒ app (rev t) (cons h nil)
end.
Compute (rev (cons 1 (cons 2 nil))).
Compute (rev (cons true nil)).
: (list X) :=
match l1 with
| nil ⇒ l2
| cons h t ⇒ cons h (app t l2)
end.
Compute (app nil (cons 1 (cons 2 nil))).
Compute (app (cons 2 (cons 7 nil)) nil).
Compute (app (cons 2 (cons 7 nil)) (cons 3 (cons 1 (cons 0 nil)))).
Fixpoint rev {X:Type} (l:list X) : list X :=
match l with
| nil ⇒ nil
| cons h t ⇒ app (rev t) (cons h nil)
end.
Compute (rev (cons 1 (cons 2 nil))).
Compute (rev (cons true nil)).
Exercise: 2 stars, standard (allb)
Complete the definition of allb : list bool → bool. It should return true if every bool in the list is true.
Fixpoint allb (l : list bool) : bool (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Compute (allb nil).
Compute (allb (cons true nil)).
Compute (allb (cons true (cons false nil))).
☐
Compute (allb nil).
Compute (allb (cons true nil)).
Compute (allb (cons true (cons false nil))).
☐
Exercise: 2 stars, standard (sum)
Complete the definition of sum : list nat → nat. It should sum up a list.
Fixpoint sum (l : list nat) : nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Compute (sum (cons 12 (cons 11 (cons 12 (cons 12 nil))))). (* ==> chirp chirp *)
☐
Compute (sum (cons 12 (cons 11 (cons 12 (cons 12 nil))))). (* ==> chirp chirp *)
☐
Exercise: 3 stars, standard (zero_to_n)
Write a function zero_to_n that builds a list from 0 to n, inclusive. Coq's termination requirements will get in your way here.
Definition zero_to_n (n : nat) : list nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Compute zero_to_n 0.
Compute zero_to_n 5.
☐
Compute zero_to_n 0.
Compute zero_to_n 5.
☐
Supplying Type Arguments Explicitly
(The Fail qualifier that appears before Definition can be
used with any command, and is used to ensure that that command
indeed fails when executed. If the command does fail, Coq prints
the corresponding error message, but continues processing the rest
of the file.)
Here, Coq gives us an error because it doesn't know what type
argument to supply to nil. We can help it by providing an
explicit type declaration (so that Coq has more information
available when it gets to the "application" of nil):
Alternatively, we can force the implicit arguments to be explicit by
prefixing the function name with @.
Polymorphic Pairs
As with lists, we make the type arguments implicit and define the
a nice concrete notation.
We can also use the Notation mechanism to define the standard
notation for product types:
(The annotation : type_scope tells Coq that this abbreviation
should only be used when parsing types, not when parsing
expressions. This avoids a clash with the multiplication
symbol.)
It is easy at first to get (x,y) and X×Y confused.
Remember that (x,y) is a value built from two other values,
while X×Y is a type built from two other types. If x has
type X and y has type Y, then (x,y) has type X×Y.
Why does one match pattern suffice? Well, prod has only one constructor! We could just as well say:
let (x, y) := p in x + y
Which is a bit nicer. If we care about what's inside the pair we may want more arms after all:
match p with
| (0, y) ⇒ y
| (x, 0) ⇒ x
| (x, y) ⇒ x × y
end
The first and second projection functions extract the parts
of pairs. We've put them in a module because you should avoid
them where possible, usually matching on the pair is much
better.
let (x, y) := p in x + y
match p with
| (0, y) ⇒ y
| (x, 0) ⇒ x
| (x, y) ⇒ x × y
end
Module PairProjection.
Definition fst {X Y : Type} (p : X × Y) : X :=
match p with
| (x, y) ⇒ x
end.
Definition snd {X Y : Type} (p : X × Y) : Y :=
let (x, y) := p in y.
Definition fst {X Y : Type} (p : X × Y) : X :=
match p with
| (x, y) ⇒ x
end.
Definition snd {X Y : Type} (p : X × Y) : Y :=
let (x, y) := p in y.
Exercise: 2 stars, standard (pair_exercises)
Definition pair_swap {X Y : Type} (pair: X × Y) : Y × X
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Compute (pair_swap (false,true)). (* Should be (true, false) *)
Compute (pair_swap (pair_swap (0, 1))). (* Should be (0, 1) *)
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Compute (pair_swap (false,true)). (* Should be (true, false) *)
Compute (pair_swap (pair_swap (0, 1))). (* Should be (0, 1) *)
Next, let's write a quirky little version of andb. Note that
this is not a polymorphic function!
Definition pair_both (pair : bool × bool) : bool (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Compute (pair_both (false,true)). (* Should be false *)
Compute (pair_both (true,true)). (* Should be true *)
Compute (pair_both (false,true)). (* Should be false *)
Compute (pair_both (true,true)). (* Should be true *)
Finally, let's write a silly function that builds a pair of alternatives from two numbers. The first will be the product of the numbers, and the second will be their sum.
Definition silly_pair (n m : nat) : nat × nat (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Compute (silly_pair 2 4). (* Should be (8, 6) *)
(* Do not modify the following line: *)
Definition manual_grade_for_pair_exercises : option (nat×string) := None.
☐
Compute (silly_pair 2 4). (* Should be (8, 6) *)
(* Do not modify the following line: *)
Definition manual_grade_for_pair_exercises : option (nat×string) := None.
☐
Exercise: 2 stars, standard, optional (currying)
In Coq, a function f : A → B → C really has the type A → (B → C). That is, if you give f a value of type A, it will give you function f' : B → C. If you then give f' a value of type B, it will return a value of type C. This allows for partial application, as in plus3. Processing a list of arguments with functions that return functions is called currying, in honor of the logician Haskell Curry.f' : nat → (nat → nat)
f'(x) = g_x(y)
g_x : nat → nat
g_x(y) = 2*x + y The subscript on g, written here as g_x, conveys the idea that g is "closed over" x. Here's an example:
f(20, 7) = 2*20 + 7 = 40 + 7 = 47
f'(20) = g_20
g_20(7) = 2*20 + 7 = 40 + 7 = 47 Functional programming languages like Gallina typically use curried functions; most other languages use uncurried functions.
As an exercise, define its inverse, prod_uncurry.
Definition prod_uncurry {X Y Z : Type}
(f : X → Y → Z) (p : X × Y) : Z
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Check @prod_curry.
Check @prod_uncurry.
(* Do not modify the following line: *)
Definition manual_grade_for_currying : option (nat×string) := None.
☐
(f : X → Y → Z) (p : X × Y) : Z
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Check @prod_curry.
Check @prod_uncurry.
(* Do not modify the following line: *)
Definition manual_grade_for_currying : option (nat×string) := None.
☐
Polymorphic Options
Compute (Some 4) + 5.
Compute app (cons 3 (cons 2 nil)) (cons 1 nil).
Definition opt_example (num_opt : option nat) : nat :=
match num_opt with
| None ⇒ 5
| Some n ⇒ n + 5
end.
Definition list_opt_example (list_opt : option (list nat)) : list nat :=
match list_opt with
| None ⇒ (cons 1 nil)
| Some lst ⇒ app lst (cons 1 nil)
end.
Module OptionPlayground.
Inductive option (X:Type) : Type :=
| Some (x : X)
| None.
Arguments Some {X} _.
Arguments None {X}.
End OptionPlayground.
Inductive option (X:Type) : Type :=
| Some (x : X)
| None.
Arguments Some {X} _.
Arguments None {X}.
End OptionPlayground.
We can now write the nth_error function, which tries to
get the nth element of a list. What should it return if there
aren't enough elements in the list? We have no way of creating a
"default" X, so we actually can't define this polymorphic
function without option X. If you don't believe me, just try!
Fixpoint nth_error {X : Type} (l : list X) (n : nat)
: option X :=
match l with
| nil ⇒ None
| cons a l' ⇒ if eqb n O then Some a else nth_error l' (pred n)
end.
Compute (nth_error (cons 4 (cons 5 (cons 6 (cons 7 nil)))) 0). (* ==> Some 4 *)
Compute (nth_error (cons (cons monday nil) (cons (cons tuesday nil) nil)) 1). (* ==> Some (cons tuesday nil) *)
Compute (nth_error (cons C nil) 2). (* ==> None *)
: option X :=
match l with
| nil ⇒ None
| cons a l' ⇒ if eqb n O then Some a else nth_error l' (pred n)
end.
Compute (nth_error (cons 4 (cons 5 (cons 6 (cons 7 nil)))) 0). (* ==> Some 4 *)
Compute (nth_error (cons (cons monday nil) (cons (cons tuesday nil) nil)) 1). (* ==> Some (cons tuesday nil) *)
Compute (nth_error (cons C nil) 2). (* ==> None *)
Exercise: 1 star, standard (hd_error)
Complete the definition of a polymorphic version of the hd_error function. It should return the first element of the list, if possible. Don't just call nth_error!
Definition hd_error {X : Type} (l : list X) : option X
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Once again, to force the implicit arguments to be explicit,
we can use @ before the name of the function.
Check @hd_error : ∀ X : Type, list X → option X.
Compute (hd_error (cons 1 (cons 2 nil))). (* ==> Some 1 *)
Compute (hd_error (cons (cons 1 nil) (cons (cons 2 nil) nil))). (* ==> Some (cons 1 nil) *)
☐
Compute (hd_error (cons 1 (cons 2 nil))). (* ==> Some 1 *)
Compute (hd_error (cons (cons 1 nil) (cons (cons 2 nil) nil))). (* ==> Some (cons 1 nil) *)
☐
Exercise: 1 star, standard (list_min)
(* FILL IN HERE *)
(* Compute (list_min (cons 3 (cons 1 (cons 5 nil)))). (* Should be Some 1 *) *)
(* Compute (list_min nil). (* Should be None *) *)
(* Do not modify the following line: *)
Definition manual_grade_for_list_min : option (nat×string) := None.
☐
(* Compute (list_min (cons 3 (cons 1 (cons 5 nil)))). (* Should be Some 1 *) *)
(* Compute (list_min nil). (* Should be None *) *)
(* Do not modify the following line: *)
Definition manual_grade_for_list_min : option (nat×string) := None.
☐
Exercise: 1 star, standard (unwrap_or)
Definition unwrap_or {X:Type} (opt:option X) (default: X) : X
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Compute (unwrap_or None 47). (* Should be 47 *)
Compute (unwrap_or (Some 4) 7). (* Should be 4 *)
☐
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
Compute (unwrap_or None 47). (* Should be 47 *)
Compute (unwrap_or (Some 4) 7). (* Should be 4 *)
☐
Exercise: 1 star, standard (option_map)
Definition option_map {X Y : Type} (f: X → Y) (opt:option X) : option Y
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
Exercise: 1 star, standard (and_then)
Definition and_then {X Y : Type} (f: X → option Y) (opt:option X) : option Y
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
For practice, let's implement option_map using and_then. No
matches allowed!
Note how and_then and option_map only differ in one place, and
that's the place that and_then allows for abstracting over!
Exercise: 1 star, standard (option_map')
Definition option_map' {X Y : Type} (f: X → Y) (opt: option X): option Y
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
(* Do not modify the following line: *)
Definition manual_grade_for_option_map' : option (nat×string) := None.
☐
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
(* Do not modify the following line: *)
Definition manual_grade_for_option_map' : option (nat×string) := None.
☐
We can even come up with option versions of boolean operators.
First, consider xor_option, which returns None if either zero or both
of the input option values are Some x, otherwise returns the
value of whichever was not None:
Exercise: 1 star, standard (xor_option)
Definition xor_option {X:Type} (o1 o2 : option X) : option X
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
Next, let's take a look at and_option, which takes an option X
and an option Y and produces an option (X × Y) if they are
both Some. You can do this in just four lines of code, not
including the type declaration!
Exercise: 1 star, standard (and_option)
Definition and_option {X Y : Type} (ox : option X) (oy : option Y) : option (X×Y)
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
(* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
More Polymorphic Functions
Definition argmin {A:Type} (cost : A → nat) (o1 o2 : A) : A :=
let c1 := cost o1 in
let c2 := cost o2 in
if leb c1 c2
then o1
else o2.
Definition argmin3 {A:Type} (cost : A → nat) (o1 o2 o3 : A) : A :=
let c1 := cost o1 in
let c2 := cost o2 in
let c3 := cost o3 in
if leb c1 c2
then if leb c1 c3
then o1
else o3
else if leb c2 c3
then o2
else o3.
let c1 := cost o1 in
let c2 := cost o2 in
if leb c1 c2
then o1
else o2.
Definition argmin3 {A:Type} (cost : A → nat) (o1 o2 o3 : A) : A :=
let c1 := cost o1 in
let c2 := cost o2 in
let c3 := cost o3 in
if leb c1 c2
then if leb c1 c3
then o1
else o3
else if leb c2 c3
then o2
else o3.
Here's another (somewhat contrived) example:
Definition select {X Y: Type} (f : X → bool) (x:X) (y1 : Y) (y2 : Y) : Y :=
if (f x) then y1 else y2.
Compute select (fun x ⇒ evenb x) 7 monday friday. (* ===> friday *)
if (f x) then y1 else y2.
Compute select (fun x ⇒ evenb x) 7 monday friday. (* ===> friday *)
Remember our favorite_day example from Day 2? Well, we could
have implemented it using select, although it's arguable whether
this is any improvement. Note that types X and Y here are
both day!
Definition favorite_day' (f: day → bool) : day :=
(* Is monday good? Use it! *)
select f monday
monday
(select f tuesday (* Otherwise, is tuesday good? Use it! *)
tuesday
(select f wednesday (* Otherwise ... *)
wednesday
(select f thursday
thursday
(select f friday
friday
(select f saturday
saturday
sunday))))).
Compute favorite_day' is_weekday. (* ===> monday *)
(* Is monday good? Use it! *)
select f monday
monday
(select f tuesday (* Otherwise, is tuesday good? Use it! *)
tuesday
(select f wednesday (* Otherwise ... *)
wednesday
(select f thursday
thursday
(select f friday
friday
(select f saturday
saturday
sunday))))).
Compute favorite_day' is_weekday. (* ===> monday *)
Finally, let's look at one more polymorphic type: result.
Let's imagine we're reading in data from files. This can go wrong in a variety of ways. In reality there are hundreds of errors we might see!
Module BadErrors.
Inductive FileError_FirstTry :=
| ok (* The not-really-an-error error error *)
| file_not_found
| permission_denied
| temporarily_unavailable
| operation_canceled.
End BadErrors.
Inductive FileError_FirstTry :=
| ok (* The not-really-an-error error error *)
| file_not_found
| permission_denied
| temporarily_unavailable
| operation_canceled.
End BadErrors.
FileError_FirstTry has that unpleasant ok value. We'd prefer not
to have to always check if a given "error" is actually ok or
not. Even worse---let's say our read_file function returns a
string × FileError_FirstTry pair: the file contents (if any, or
else the empty string) and the error (if any, or else ok).
This is essentially the choice that the go programming
language makes in its standard library.
But this is gnarly! You have to remember to check for ok all
the time, sometimes the string you get back has a meaning and
sometimes it doesn't---it's just a recipe for disaster.
We can use a better pair of types to help us out of this mess:
Inductive result (OkType ErrType:Type) : Type :=
| Ok (x : OkType)
| Error (x : ErrType).
Arguments Ok {OkType ErrType} _.
Arguments Error {OkType ErrType} _.
Inductive FileError :=
| file_not_found
| permission_denied
| temporarily_unavailable
| operation_canceled.
| Ok (x : OkType)
| Error (x : ErrType).
Arguments Ok {OkType ErrType} _.
Arguments Error {OkType ErrType} _.
Inductive FileError :=
| file_not_found
| permission_denied
| temporarily_unavailable
| operation_canceled.
Now result can either be a success value or a failure value, and we parameterize it both on the type of success value and the type of error:
In real life we'd read the file in from path, checking for
errors, and return something appropriate. Here we'll just
return an error:
This result type indicates not only success or failure (like
an option), but also a reason for the failure. It can be useful
in all kinds of places: Parsing strings into numbers, compiling
programs, acting on user input, and more.
If we don't mind losing the reason for the failure, we can
convert a result to an option:
Definition result_to_option {X Err : Type} (r:result X Err) : option X :=
match r with
| Ok x ⇒ Some x
| Error err ⇒ None
end.
match r with
| Ok x ⇒ Some x
| Error err ⇒ None
end.
Remember our option combinators like option_map? We can do
the same kinds of things with result.
result_map is like option_map in that it passes along a
failure unchanged, but a success gets transformed by the given
function.
Exercise: 1 star, standard (result_map)
Definition result_map {X Y Err:Type} (f : X → Y) (r: result X Err) : result Y Err (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
☐
Next, let's mess around with nested results and options.
result_transpose goes from a result (option X) Err to an
option (result X Err). Ok None should map to just
None. This kind of type plumbing can be useful for long chains
of combinators!
Exercise: 1 star, standard (result_transpose)
Definition result_transpose {X Err : Type} (r : result (option X) Err) : option (result X Err) (* REPLACE THIS LINE WITH ":= _your_definition_ ." *). Admitted.
☐
☐