Tags: /
coq
/
In programming languages, a list is a collection of values.
The built-in lists of some programming languages (like OCaml and C++) are homogeneous lists. ie, lists where all elements should be of the same type.
Whereas the lists that come out of the box in some other programming languages (like Python) can be heterogeneous, where the elements needn't all be of the same type. (I included a sample usage of lists in a few programming languages under the 'Addendum' section at the end of this blog post.)
In Coq, we have built-in lists in the form of list
, which is defined as an inductive type:
Inductive list (A : Type) : Set :=
| nil : list A
| cons : A -> list A -> list A.
Arguments nil {A}.
Arguments cons {A} a l.
As is clear from the definition, this list allows only elements of the same type. ie, it's a homogeneous list.
Examples of using Coq's list
:
Require Import List.
Import ListNotations.
Check [1; 2].
(* [1; 2] : list nat *)
Check [true; false].
(* [true; false] : list bool *)
Coq's type checker won't be happy if we try making a non-homogenous list with list
:
Check [1; true].
(*
The term "[true]" has type "list bool" while it is expected to have type
"list nat".
*)
Check [true; 1].
(*
The term "[1]" has type "list nat" while it is expected to have type
"list bool".
*)
In the list
type, the type of the elements of the list is declared as an implicit argument. So Coq looks at the first element of the list and expects every other element to be of the same type.
A way to have heterogeneous lists (hlists) in Coq is described in the book Certified Programming with Dependent Types.
I had found it a bit difficult to understand at first, so I describe the way that helped me understand it in this blog post.
hlist
typeHere's an inductive type representing hlist
values:
Inductive hlist (A : Type) (B : A -> Type) : list A -> Type :=
| hnil : hlist A B []
| hcons : forall (x : A) (ls : list A),
(B x) -> hlist A B ls -> hlist A B (x::ls).
In this definition, the hlist
type has a list associated with it (the list A
).
We also have B
, which is a function that maps the actual type of the elements of the hlist to a common type. It does a 'translation' to make Coq sort of feel that all elements are of the same type so that the type checker will stay happy.
The elements of the list A
are passed as arguments to the B
function to get the type of the corresponding hlist element. ie, the i-th element of the list A
is passed to B
to get a type corresponding to the i-th element of the hlist.
The way the hlist
will turn out depends on our choice of B
. I took (fun x:Set => Type)
as the value of B
(since Type
is the type of all types).
This is just one way of doing this. Coq just needs all elements to appear to be of the same type, one way or the other.
Length of list in hcons
's type is same as the number of elements of the hlist to which a new element is being added. Quite like the case of list
.
(This often confuses me. I somehow keep thinking that this list is associated with the entire list put together by hcons
instead of just its hlist
argument to which a new element is being added.)
Let's have a look at the arguments needed by the hlist
constructors.
hcons
needs the following arguments:
Argument | Comment |
---|---|
A : Type |
Type of elements of ls |
B : A -> Type |
Mapping from A to Type |
x : A |
Value to pass to B for new element val |
ls : list A |
Values to be passed to B for xs |
val : B x |
New hlist element. Its type is B x |
xs : hlist |
Remaining elements |
(I gave names val
and xs
for convenience in the above table.)
The 'translation' performed by the B
function sort of makes it looks as if all elements of the hlist are of type Type
.
A simple hcons
constructor usage looks like:
hcons Set (fun x => x) nat []%list 3 hnil.
| | | | | | | | | | |
+-+ +----------+ +-+ +-----+ | +--+
| | | | | |
| B:A->Type | ls:list A | hlist A B _ _
| | |
A:Type x:A B x
(Note: Length of the ls
here is one less than the length of the resultant hlist
itself.)
Compute hcons.
(*
= fun (A : Type) (B : A -> Type) (x : A) (ls : list A)
(x0 : B x) (x1 : hlist A B ls) => hcons A B x ls x0 x1
: forall (A : Type) (B : A -> Type) (x : A) (ls : list A),
B x -> hlist A B ls -> hlist A B (x :: ls)
*)
Compute hcons Set. (* A (hlist) *)
(*
= fun (B : Set -> Type) (x : Set) (ls : list Set)
(x0 : B x) (x1 : hlist Set B ls) => hcons Set B x ls x0 x1
: forall (B : Set -> Type) (x : Set) (ls : list Set),
B x -> hlist Set B ls -> hlist Set B (x :: ls)
*)
Compute hcons Set (fun x => x). (* B (hlist) *)
(*
= fun (x : Set) (ls : list Set) (x0 : (fun x0 : Set => x0) x)
(x1 : hlist Set (fun x1 : Set => x1) ls) =>
hcons Set (fun x2 : Set => x2) x ls x0 x1
: forall (x : Set) (ls : list Set),
(fun x0 : Set => x0) x ->
hlist Set (fun x0 : Set => x0) ls ->
hlist Set (fun x0 : Set => x0) (x :: ls)
*)
Compute hcons Set (fun x => x) nat. (* x:A *)
(*
= fun (ls : list Set) (x : (fun x : Set => x) nat)
(x0 : hlist Set (fun x0 : Set => x0) ls) =>
hcons Set (fun x1 : Set => x1) nat ls x x0
: forall ls : list Set,
(fun x : Set => x) nat ->
hlist Set (fun x : Set => x) ls ->
hlist Set (fun x : Set => x) (nat :: ls)
*)
Compute hcons Set (fun x => x) nat []%list. (* ls:list A *)
(*
= fun (x : (fun x : Set => x) nat)
(x0 : hlist Set (fun x0 : Set => x0) []) =>
hcons Set (fun x1 : Set => x1) nat [] x x0
: (fun x : Set => x) nat ->
hlist Set (fun x : Set => x) [] -> hlist Set (fun x : Set => x) [nat]
*)
Compute hcons Set (fun x => x) nat []%list 3. (* B x *)
(*
= fun x : hlist Set (fun x : Set => x) [] =>
hcons Set (fun x0 : Set => x0) nat [] 3 x
: hlist Set (fun x : Set => x) [] -> hlist Set (fun x : Set => x) [nat]
*)
Compute hcons Set (fun x => x) nat []%list 3
(hnil Set (fun x => x)).
(*
= hcons Set (fun x : Set => x) nat [] 3 (hnil Set (fun x : Set => x))
*)
(Well, the output is quite verbose and hence not so readable. We can make this better with notations. We'll do that later.)
hnil
takes just the A
and B
as arguments.
A sample usage of the hnil
constructor:
hnil Set (fun x => x)
| | | |
+-+ +----------+
| |
| B:A->Type
|
A:Type
As evident from:
Compute hnil.
(*
= fun (A : Type) (B : A -> Type) => hnil A B
: forall (A : Type) (B : A -> Type), hlist A B []
*)
(* Two more arguments needed: A and B. *)
Compute hnil Set.
(*
= fun B : Set -> Type => hnil Set B
: forall B : Set -> Type, hlist Set B []
*)
(* One more argument remaining: B *)
Compute hnil Set (fun x => x).
(*
= hnil Set (fun x : Set => x)
: hlist Set (fun x : Set => x) []
*)
(* Okay, that's it. *)
Now, let's define a few function for working with hlist
values.
But before that, it's good to have a few example hlists that can be used to try out these functions:
(* [[ 3; true ]] *)
Example hls1 : hlist Set (fun x => x) [nat; bool] :=
hcons Set (fun x=>x) nat [bool] 3
(hcons Set (fun x=>x) bool [] true (hnil _ _)).
(* [[ false; 3; true ]] *)
Example hls2 : hlist Set (fun x => x) [bool; nat; bool] :=
hcons Set (fun x=>x) bool[nat;bool] false
(hcons Set (fun x=>x) nat [bool] 3
(hcons Set (fun x=>x) bool [] true (hnil _ _))).
While defining functions for hlist
, we must bear in mind that Gallina functions need to be total. ie, all possibilities need to be considered.
To satisfy this criterion while still keeping the type checker happy, the functions whose definitions are given below return unit
type when a value cannot be returned (denoting an abnormal/error condition).
Calculating the length of an hlist
is quite easy as the hlist
type itself carries the information needed to find its length.
Definition hlength {A:Type} {B:A->Type}
{ls : list A} (hls : hlist A B ls) : nat := length ls.
hlength
merely takes ls
which is part of the hlist
type and returns its length.
Sample usage:
Compute hlength hls1.
(* 2 : nat *)
Function to get the first element of an hlist
.
Definition hhead {A: Type} {B: A -> Type}
{ls: list A} (hls : hlist A B ls)
: match ls with
| (t::ts)%list => B t
| _ => (unit:Type)
end :=
match hls with
| hnil _ _ => tt
| hcons _ _ typ typs x xs => x
end.
hhead
returns tt : unit
if the argument hlist
is empty.
Sample usage:
Compute hhead hls1.
(*
= 3
: match [nat; bool]%list with
| []%list => unit : Type
| (t :: _)%list => (fun x : Set => x) t
end
*)
Function that gets all elements of an hlist
except the first element.
Definition htail {A: Type} {B: A -> Type}
{ls: list A} (hls : hlist A B ls)
: match ls with
| (t::ts)%list => hlist A B ts
| _ => hlist A B []%list
end :=
match hls with
| hnil _ _ => hnil _ _
| hcons _ _ typ typs x xs => xs
end.
Sample usage:
Compute htail hls1.
(*
= hcons Set (fun x : Set => x) bool [] true
(hnil Set (fun x : Set => x))
: match [nat; bool]%list with
| []%list => hlist Set (fun x : Set => x) []
| (_ :: ts)%list => hlist Set (fun x : Set => x) ts
end
*)
(* ie, an hlist with 'true' as its only element. *)
Compute htail (htail hls1).
(*
= hnil Set (fun x : Set => x)
: match [bool]%list with
| []%list => hlist Set (fun x : Set => x) []
| (_ :: ts)%list => hlist Set (fun x : Set => x) ts
end
*)
(* ie, an empty hlist *)
Function to return the i
-th element of an hlist
.
Indexing starts from 0
.
Fixpoint hidx_helper {A : Type} (B:A->Type)
(ls : list A) (idx : nat) : Type :=
match idx, ls with
| _, []%list => unit
| O, (x::xs)%list => B x
| S n', (x::xs)%list => hidx_helper B xs n'
end.
Fixpoint hidx {A: Type} {B: A -> Type} {ls: list A}
(idx : nat) (hls : @hlist A B ls)
: hidx_helper B ls idx :=
match idx, hls in (hlist _ _ ls) return
(hidx_helper B ls idx) with
| _, @hnil _ _ => tt
| O, @hcons _ _ _ _ x xs => x
| S n', @hcons _ _ _ _ x xs => hidx n' xs
end.
A helper function is used to calculate the return type. Not sure how good a definition this is, as the return type of hidx
will contain the hidx_helper
. But it does seem to work. 😊
Sample usage:
Compute hidx 0 hls2.
(*
= false
: hidx_helper (fun x : Set => x) [bool; nat; bool] 0
*)
Compute hidx 1 hls2.
(*
= 3
: hidx_helper (fun x : Set => x) [bool; nat; bool] 1
*)
Compute hidx 2 hls2.
(*
= true
: hidx_helper (fun x : Set => x) [bool; nat; bool] 2
*)
Compute hidx 3 hls2.
(*
= tt
: hidx_helper (fun x : Set => x) [bool; nat; bool] 3
*)
Function to append two hlist
values.
Fixpoint happ {A:Type} {B:A->Type} {ls1 ls2: list A}
(hls1 : hlist A B ls1) (hls2: hlist A B ls2) :
hlist A B (ls1++ls2)%list :=
match hls1 in (hlist _ _ ls1) return
hlist A B (ls1++ls2)%list with
| hnil _ _ => hls2
| hcons _ _ _ _ y ys =>
hcons A B _ _ y (happ ys hls2)
end.
The second hlist
is added to the end of the first hlist
.
Sample usage:
Compute happ hls1 hls2.
(* [[ 3; true; false; 3; true ]] *)
(*
= hcons Set (fun x : Set => x) nat [bool; bool; nat; bool] 3
(hcons Set (fun x : Set => x) bool [bool; nat; bool] true
(hcons Set (fun x : Set => x) bool [nat; bool] false
(hcons Set (fun x : Set => x) nat [bool] 3
(hcons Set (fun x : Set => x) bool [] true
(hnil Set (fun x : Set => x))))))
: hlist Set (fun x : Set => x) ([nat; bool] ++ [bool; nat;
bool])
*)
The output that we saw so far were often too complex or unreadable. In addition we need to write stuff like that to make hlist values as well.
To have a more readable hlist
representation, we can use Coq notations.
We can think of these notations as doing simple textual replacement. We can make short notations for bigger definitions.
All right, let's make a few notations.
Module HListNotations.
(* A new scope for the hlist notations. *)
Delimit Scope hlist_scope with hlist.
Notation "[[ ]]" := (hnil _ _) : hlist_scope.
Notation "[[ x ]]" :=
(hcons Set (fun y=>y) _ _ x (hnil _ _)) : hlist_scope.
Notation "[[ x , y , .. , z ]]" :=
(hcons Set (fun a=>a) _ _ x
(hcons Set (fun a=>a) _ _ y ..
(hcons Set (fun a=>a) _ _ z
(hnil _ _)) .. )) : hlist_scope.
Infix "::" := (hcons Set (fun x=>x) _ _)
(at level 60, right associativity, only parsing) : hlist_scope.
Infix "++" := happ
(at level 60, right associativity, only parsing) : hlist_scope.
End HListNotations.
Placed all these notations into a module named HListNotations
(under a new scope with 'hlist' as key).
This module can then be imported with
Import HListNotations.
Example usage of these notations:
Import HListNotations.
(* Empty hlist *)
Compute [[ ]]%hlist.
(*
= [[ ]]%hlist
: hlist ?A ?B []
*)
(* Single element hlist *)
Compute [[ 3 ]]%hlist.
(*
= [[3]]%hlist
: hlist Set (fun y : Set => y) [nat]
*)
(* Multiple element hlists *)
Compute [[ 2, 1 ]]%hlist.
(*
= [[2, 1]]%hlist
: hlist Set (fun a : Set => a) [nat; nat]
*)
Compute [[ 3, 2, 1 ]]%hlist.
(*
= [[3, 2, 1]]%hlist
: hlist Set (fun a : Set => a) [nat; nat; nat]
*)
(* Few 'non-simple' hlists *)
Compute [[ (1, "hi"%string), 3]]%hlist.
(*
= [[(1, "hi"%string), 3]]%hlist
: hlist Set (fun a : Set => a) [(nat * string)%type; nat]
*)
Compute [[ (1, true, false), 3]]%hlist.
(*
= [[(1, true, false), 3]]%hlist
: hlist Set (fun a : Set => a) [(nat * bool * bool)%type; nat]
*)
Compute [[ (1, (2, (3, 4) ), false), 3]]%hlist.
(*
= [[(1, (2, (3, 4)), false), 3]]%hlist
: hlist Set (fun a : Set => a)
[(nat * (nat * (nat * nat)) * bool)%type; nat]
*)
(* Add an element to a hlist *)
Compute (3 :: [[]])%hlist.
(*
= [[3]]%hlist
: hlist Set (fun x : Set => x) [nat]
*)
Compute hhead ([[ 3 ]]%hlist).
(*
= 3
: match [nat] with
| [] => unit : Type
| (t :: _)%list => (fun y : Set => y) t
end
*)
(* Append two hlists *)
Compute (hls1 ++ hls2)%hlist.
(*
= [[3, true, false, 3, true]]%hlist
: hlist Set (fun x : Set => x) ([nat; bool] ++ [bool; nat; bool])
*)
Compute ([[1, (2, true)]] ++ [[5]])%hlist.
(*
= [[1, (2, true), 5]]%hlist
: hlist Set (fun a : Set => a) ([nat; (nat * bool)%type] ++ [nat])
*)
Compute ([[1, true]]++[["hi"%string]])%hlist.
(*
= [[1, true, "hi"%string]]%hlist
: hlist Set (fun a : Set => a) ([nat; bool] ++ [string])
*)
Though we now got notations, we might still need to use the hlist constructors directly.
To make such explicit usages easier, let us make a few of the constructor parameters implicit.
hnil
needs the following arguments:
Argument | Comment |
---|---|
A : Type |
Obtainable from B |
B : A -> Type |
Irrelevant as list is empty |
Making both A
and B
implicit,
Arguments hnil {A B}.
Now hnil
can also be used like:
Check @hnil _ _.
Check hnil.
(*
[[ ]]%hlist
: hlist ?A ?B []
where
?A : [ |- Type]
?B : [ |- ?A -> Type]
*)
In the case of hcons
, the following arguments are needed:
Argument | Comment |
---|---|
A : Type |
Available from B |
B : A -> Type |
Available from hlist type |
x : A |
Available from val |
ls : list A |
Inferable from xs and B |
val : B x |
Element being added |
xs : hlist |
Remaining elements |
(Here, I used xs
as the name of the hlist
to which an element is added with hcons
and val
to refer to the new element being added.)
Making A
, B
, x
and ls
implicit,
Arguments hcons {A B x ls}.
hcons
may now be used like:
Check hcons 3 hnil : hlist Set (fun x=>x) [nat].
Check hcons 3 hnil : hlist _ (fun x=>x) _.
(*
[[3]]%hlist : hlist Set (fun x : Set => x) [nat]
: hlist Set (fun x : Set => x) [nat]
*)
instead of
Check @hcons Set (fun x=>x) nat []%list 3 hnil
: hlist Set (fun x=>x) [nat].
Check @hcons Set (fun x=>x) nat []%list 3 hnil.
Observe that while using hcons
with implicit arguments, we had to provide the hlist
's type as well, whereas we needn't do that for the non-implicit argument version. This is due to the way in which our notation was defined. If the type of hlist
is not provided, Coq has no way of figuring out the values of arguments declared as implicit.
We could also make a few arguments of the hlist
type implicit as well.
hlist
needs the following arguments:
Argument | Comment |
---|---|
A : Type |
Available from B |
B : A -> Type |
|
list A |
Inferable via B and elements |
Let's make A
and the list A
implicit, while keeping B
as it is.
Arguments hlist {A} _ {ls}.
Now we can have:
Check hlist (fun x=>x).
(*
hlist (fun x : Type => x)
: Type
where
?ls : [ |- list Type]
*)
Check hcons 3 hnil : hlist (fun x=>x).
(*
hcons 3 [[ ]]%hlist : hlist (fun x : Type => x)
: hlist (fun x : Type => x)
*)
Right, that looks much more concise than what we started off with.
As was the case with notations, this is just one way doing things. There could be other (better) ways.
All the definitions that we made.
Included the functions as well as the notations makes use of some them.
This version of the functions makes some use of the implicit arguments that we had declared.
Require Import List.
Import ListNotations.
(* hlist type *)
Inductive hlist (A : Type) (B : A -> Type) : list A -> Type :=
| hnil : hlist A B []
| hcons : forall (x : A) (ls : list A),
(B x) -> hlist A B ls -> hlist A B (x::ls).
Arguments hnil {A B}.
Arguments hcons {A B x ls}.
Arguments hlist {A} _ {ls}.
(* Functions *)
Definition hlength {A:Type} {B:A->Type}
{ls : list A} (hls : @hlist A B ls) : nat := length ls.
Definition hhead {A: Type} {B: A -> Type}
{ls: list A} (hls : @hlist A B ls)
: match ls with
| (t::ts)%list => B t
| _ => (unit:Type)
end :=
match hls with
| hnil => tt
| hcons x xs => x
end.
Definition htail {A: Type} {B: A -> Type}
{ls: list A} (hls : @hlist A B ls)
: match ls with
| (t::ts)%list => hlist B
| _ => hlist B
end :=
match hls with
| hnil => hnil
| hcons x xs => xs
end.
Fixpoint hidx_helper {A : Type} (B:A->Type)
(ls : list A) (idx : nat) : Type :=
match idx, ls with
| _, []%list => unit
| O, (x::xs)%list => B x
| S n', (x::xs)%list => hidx_helper B xs n'
end.
Fixpoint hidx {A: Type} {B: A -> Type} {ls: list A}
(idx : nat) (hls : hlist B)
: hidx_helper B ls idx :=
match idx, hls in (@hlist _ _ ls) return
(hidx_helper B ls idx) with
| _, hnil => tt
| O, hcons x xs => x
| S n', hcons x xs => hidx n' xs
end.
Fixpoint happ {A:Type} {B:A->Type} {ls1 ls2: list A}
(hls1 : @hlist A B ls1) (hls2: @hlist A B ls2) :
@hlist A B (ls1++ls2)%list :=
match hls1 in (@hlist _ _ ls1) return
@hlist A B (ls1++ls2)%list with
| hnil => hls2
| hcons y ys =>
hcons y (happ ys hls2)
end.
(* Notations *)
Module HListNotations.
Delimit Scope hlist_scope with hlist.
Notation "[[ ]]" := (@hnil _ _) : hlist_scope.
Notation "[[ x ]]" :=
(@hcons Set (fun y=>y) _ _ x (@hnil _ _)) : hlist_scope.
Notation "[[ x , y , .. , z ]]" :=
(@hcons Set (fun a=>a) _ _ x
(@hcons Set (fun a=>a) _ _ y ..
(@hcons Set (fun a=>a) _ _ z
(@hnil _ _)) .. )) : hlist_scope.
Infix "::" := (@hcons Set (fun x=>x) _ _)
(at level 60, right associativity, only parsing) : hlist_scope.
Infix "++" := happ
(at level 60, right associativity, only parsing) : hlist_scope.
End HListNotations.
Thanks to Gaëtan Gilbert for help in setting the indexing function right. 🙂
Lists in OCaml:
(* integer list *)
# 1;2;3];;
# [int list = [1; 2; 3]
- :
(* boolean list *)
# true;false];;
# [bool list = [true; false]
- :
(* mixed type list gives error *)
# 1;true];;
# [type bool but an expression was expected of type Error: This expression has
There are ways to have hlists in OCaml similar to Coq, though. See here for a discussion on that.
Lists in C++: One way of having lists in C++ is by using list
from its standard library.
An example program using list
:
// Make an integer list and display its elements
#include<iostream>
#include<list>
int main()
{std::list<int> l = {1, 2, 3};
for (int elem : l )
{std::cout << elem << " ";
}std::cout << "\n";
return 0;
}
This would give 1 2 3Â
as output.
Lists in Python: Python lists can be heterogeneous.
# integer list
>>> [1, 2, 3]
1, 2, 3]
[
# list with integers and a bool
>>> [1, True, 2]
1, True, 2]
[
# list with integer, float, string and bool
>>> [1, 3.1415926, "hi", True]
1, 3.1415926, 'hi', True] [
hlist
with 'narrower' B
We had used (fun x:Set => Type)
as the value of B
throughout the blog post. It's possible to have narrower types as well.
Check @hcons nat (fun x:nat => nat) 0 []%list 3 hnil.
(*
hcons 3 [[ ]]%hlist
: hlist (fun _ : nat => nat)
*)
Check @hcons bool (fun x:bool => nat) false []%list 3 hnil.
(*
hcons 3 [[ ]]%hlist
: hlist (fun _ : bool => nat)
*)