How a real module system should work.
I’ve been playing with the Coq proof assistant over the past few days, following closely on some frustrations that I’ve been having with using SML’s module system and a bit of toying with type-classes in Haskell.
The gist of the problem is this. Although you can define type-classes and modules such that external users of these modules/type-classes see a uniform interface, consistency is left as an exercise for the implementer. This is not really acceptable in my view. When you are writing software, often times *you* are the implementer. What you really want is for these modules not just to provide a consistent interface to outsiders, but to guarantee the correctness of the implementation! Isn’t that the whole point of types? If we can’t do that, why are we using types?
Ok, so in Coq I *can* get the properties I’ve been wanting out of SML’s module system. For instance take the following implementation of the Monad signature:
Module Type MONAD. Set Implicit Arguments. Parameter M : forall (A : Type), Type. Parameter bind : forall (A B : Type), M A -> (A -> M B) -> M B. Parameter ret : forall (A : Type), A -> M A. Infix ">>=" := bind (at level 20, left associativity) : monad_scope. Open Scope monad_scope. Axiom left_unit : forall (A B : Type) (f : A -> M B) (a : A), (ret a) >>= f = f a. Axiom right_unit : forall (A B : Type) (m : M A), m >>= (fun a : A => ret a) = m. Axiom bind_assoc : forall (A B C : Type) (m : M A) (f : A -> M B) (g : B -> M C) (x : B), (m >>= f) >>= g = m >>= (fun x => (f x) >>= g). End MONAD.
This signature describes something much like the monad that is given by the type-class in haskell. I neglected some stuff like implementing join from bind etc, but we can safely ignore that for now. The point is that users of the MONAD signature can’t just fake a monad by supplying an implementation that is nominally the same. i.e. In order to implement this MONAD you actually have to have the right signature for “>>=” *AND* you have to satisfy the monad laws. So what does an implementation look like? Here is an example:
Module ListMonad < : MONAD.
Require Import List.
Set Implicit Arguments.
Definition M := list.
Fixpoint bind (A : Type) (B : Type) (l : M A) (f : A -> M B) {struct l} : M B :=
match l with
| nil => nil
| h::t => (f h)++(bind t f)
end.
Infix “>>=” := bind (at level 20, left associativity) : monad_scope.
Open Scope monad_scope.
Definition ret (A : Type) := fun a : A => a::nil.
Lemma left_unit : forall (A B : Type) (f : A -> M B) (a : A),
(ret a) >>= f = f a.
Proof.
intros. simpl. rewrite app_nil_end. reflexivity.
Defined.
Lemma right_unit : forall (A B : Type) (m : M A),
m >>= (fun a : A => ret a) = m.
Proof.
simple induction m.
simpl. reflexivity.
intros. simpl.
cut (bind l (fun a0 : A => ret a0) = l).
intros. rewrite H0. reflexivity.
exact H.
Defined.
Lemma bind_assoc : forall (A B C : Type) (m : M A) (f : A -> M B) (g : B -> M C) (x : B),
(m >>= f) >>= g = m >>= (fun x => (f x) >>= g).
Proof.
simple induction m.
intros. simpl. reflexivity.
intros. simpl.
cut (l >>= f >>= g = l >>= (fun x0 : A => f x0 >>= g)).
intros. rewrite < - H0.
induction (f a).
simpl. reflexivity.
simpl. rewrite IHm0. rewrite app_ass. reflexivity.
apply H. exact x.
Defined.
End ListMonad.
(* Example *)
Import ListMonad.
Require Import Peano.
Require Import List.
Fixpoint downfrom (n : nat) {struct n} : (list nat) :=
match n with
| 0 => n::nil
| S m => n::(downfrom m)
end.
Eval compute in (1::2::3::4::nil) >>= downfrom.
= 1 :: 0 :: 2 :: 1 :: 0 :: 3 :: 2 :: 1 :: 0 :: 4 :: 3 :: 2 :: 1 :: 0 :: nil
: M nat
Ok, That took me about an hour to write. I’m not really that good at using Coq, so presumably you could do this more elegantly and in less time. In any case it would be nice if the proofs could be automated a bit more. That aside this is a *much* better situation than we have in SML and Haskell. We have provided a monad that is guaranteed to actually be one!
I’m of the growing opinion that software that is forced to meet specifications will end up being less trouble in the end than the current state of free-wheeling wild-west style implementation.
Coq gives a civilized alternative to the current free-for-all. Coq can help us make good on the promise that “well typed programs can’t go wrong”.
May 25th, 2007 at 8:25 am
Naturally, I agree with you that providing properties is a great thing. But it’s not really the module system that does this for you. It’s the fact that Coq has dependent types that allows you to do this. In fact, once you have dependent types you can code up your own module system from records (like I did in Cayenne).
June 14th, 2007 at 12:41 am
I wasn’t intending to say that properties are given by modules, but that a real module system should ensure that the necessary properties are satisfied or it is really only satisfying a nominal requirement.
February 8th, 2008 at 2:31 pm
so when do i hit the lottery so i can get together people who want to make software that sucks less? grn.
February 10th, 2008 at 3:34 am
You can do it typeclass-style in Coq too now, as I’ve been implementing the thin layer that was needed to make them painless to use in Coq.
Here’s what your example would look like: http://www.lri.fr/~sozeau/scratch/monad.v