[Agda] Re: [Coq-Club] Re: Agda beats Coq (was: Termination proof in partiality monad)

Dan Doel dan.doel at gmail.com
Sun Nov 23 04:25:51 CET 2008


On Saturday 22 November 2008 1:23:52 pm Aaron Bohannon wrote:
> I have been trying to working coinductive data and definitions
> recently and have found that Coq been very helpful in developing
> intuition about this sort of thing.  So I found your remarks below
> very interesting.  Specifically, you seem to imply that a
> CoInductive/codata definition in Coq/Agda permits some operations that
> an abstract type with an "observe" function would not permit.  Can you
> (or anyone else) be more specific about what these would be?

(It should be noted that I have experience only with Agda's codata, so I'm 
simply assuming that Coq's CoInductive is about the same.)

I wouldn't say that one permits operations that the other doesn't. I'd rather 
say that the intuitions promoted by one are better or worse for helping write 
correct proofs/reasoning about your programs. Specifically, it leads one (me at 
least) to think that various expressions involving codata should behave like 
their syntactically-almost-identical analogues with data/Inductive, *but they 
don't*. Perhaps the biggest problem is using the codata coconstructors (for 
lack of a better term) as if they were data constructors.

Consider the type of natural numbers:

    data Nat : Set where
      zero : Nat
      suc  : Nat -> Nat

And suppose we have:

  i, j, k : Nat 
  j = suc i
  k = suc i

And we want to know if j == k. Well, this is easy, because we see that j and k 
were both built using i and the suc constructor. That's how inductive data 
works, two values are the same if they were *built* the same way.

So now we turn to the codata analogue:

    codata Conat : Set where
      cozero : Conat
      cosuc  : Conat -> Conat

And we consider:

  i, j : Conat
  i ~ cosuc i -- ~ is used in Agda instead of = for corecursive definitions
  j ~ cosuc i

And we want to know if i and j are the same. Now, the syntax here might lead 
one (me certainly) to think that this is just trivial. i and j are *built* the 
same way, so they must be the same, just like inductive data. However, that's 
*not* how codata works. Two coinductive values are the same if *they produce 
the same series of observations*. So, proving i and j are the same is no 
longer trivial, and you have to talk about how one is a bisimulation of the 
other and such.

If we look at the categorical semantics of the two types, we may figure out 
where things are going wrong. Inductive types can be given initial algebra 
semantics, where Nat above goes something like:

  N X = 1 + X is a functor

  (A, a : N A -> A) is an algebra for that functor

  Nat is an initial object in the category of such algebras, which means that
  for any other algebra that fits the above form, there is a unique morphism
  from the Nat algebra to it. This looks like (using Agda for notation):
    cataNat :  {A : Set} -> A -> (A -> A) -> Nat -> A

And "cataNat" should be a pretty familiar function:

  cataNat z s zero    = z
  cataNat z s (suc n) = s (cataNat z s n)

So, just to be explicit, using inductive constructors to build inductive 
values corresponds to applying the morphism of the algebra, and pattern 
matching corresponds to using the unique morphism whose existence was proven 
by initiality. Now, consider coinductive types, which are given by terminal 
coalgebra semantics:

  C X = 1 + X is a functor

  (A, a : A -> C A) is a coalgebra for that functor

  Conat is a terminal object in the category of such algebras, which means
  that for any other coalgebra that fits the above form, there is a unique
  morphism from it to Conat. This looks like:
    anaConat : {A : Set} -> (A -> 1 + A) -> A -> Conat

Which looks like:

  anaConat f a with f a
  ... | inl () ~ cozero
  ... | inr a' ~ cosuc (anaConat f a')

However, there's a subtle difference in the way this corresponds to the 
categorical semantics that differs from the inductive case. While in the 
inductive case, application of the algebra morphism corresponded to building a 
value with a constructor, in the coinductive case, *it corresponds to 
destructing a coinductive value and looking at it*. In other words, pattern 
matching. In short, one might say that for inductive data, building is simple, 
and destroying is complex, whereas the opposite is true for coinductive 
codata.

And, I think that the coinductive syntax is misleading on this front, when you 
see:

  j ~ cosuc i

You may think, "j is built from an i and a cosuc constructor", but *this is 
not the case*. Rather the "cosuc" used as a constructor is better thought of 
as something like:

  cosuc : Conat -> Conat
  cosuc n = anaConat f (Just n)

  f : Maybe Conat -> 1 + Maybe Conat
  f (Just n) with observe n
  ... | inl () = inr Nothing
  ... | inr n' = inr (Just n')
  f Nothing = inl ()

Which has the net effect of producing one additional 'inr' observation beyond 
whatever the original Conat produced. Of course, this is probably overly 
elaborate, but the point is that the value in question cannot simply be 
thought of as trivially as the previous value with another constructor box 
wrapped around it.

(I realize this is probably confusing, since I just defined anaConat in terms 
of cozero and cosuc. It might help to look at Anton Setzer's proposed 
coalgebraic syntax for corecursive functions, where instead of working with 
pseudo-constructors like cosuc, you simply describe the observations you see. 
Then you can define the pseudo-constructors and corecursion in terms of this 
primitive:

  cozero *     = inl ()
  (cosuc n) * = inr n

  (anaConat f a) * = bimap id (anaConat f) (f a)

where:

  bimap : {a b c d : Set} -> (a -> c) -> (b -> d) -> a + b -> c + d
  bimap f _ (inl a) = inl (f a)
  bimap _ g (inr b) = inr (g b)

Hopefully that's clear.)

And, of course, this explains why indexing with specialized codata 
'constructors' in a family doesn't seem to work very well, because:

  data Foo : Conat -> Set where
    bar : {c : Conat} -> Foo c -> Foo (cosuc c)

Is not simply working with constructors. It'd be akin to writing something 
like:

  data Baz : Nat -> Set where
    blorf : {n : Nat} -> Foo n -> Foo (quux n)

Where quux is some arbitrary function. Except the situation is probably even 
worse, because normalization of quux n probably helps (I'm not exactly sure 
what the dual situation situation with inductive data is. Perhaps Foo (quux n) 
-> Foo n, where you'd have to compute the inverse of quux, or otherwise prove 
something onerous merely to construct a value), whereas you can't simply turn 
a coinductive value into some normal form consisting of only constructors 
(since it's potentially infinite).

So, to sum up, it seems to me that codata/CoInductive encourages one to think 
about coinductive values in the same way as inductive values despite that that 
leads to mistakes. The coalgebraic formulation and explicit observation (or 
not; Anton Setzer explained how to think of the current syntax in terms of 
his, and Nils Anders Danielsson showed here earlier how you can use pattern 
matching on coinductive values in lieu of a destruction operation, since 
pattern matching is the codata operation that corresponds to the coalgebra 
morphism) is closer to the actual semantics, and seems to stop these think-os 
from happening (rather than fixing up an inductive-like use with some 
explicitly placed evaluation, like you see in some of the original proofs for 
the partiality monad).

Anyhow, I think that's about the end of my rambling. Hopefully that was useful 
to you or someone else, but even if not, it helped me to get my thoughts on 
the situation in order. As a caveat, I should note that the categorical 
semantics above are only good for inductive and coinductive types, not 
inductive and coinductive families and such that you'll find in Coq and Agda. 
The relevant literature for the latter is a bit above my head at the moment. 
Nevertheless, I think it's good enough to elucidate the source of the problems 
people are having currently.

Also, if anything I said above was incorrect, I hope someone will correct me. 
:)

Cheers,
-- Dan



More information about the Agda mailing list