Many catamorphisms seem to be simple enough, mostly replacing each data constructor with a custom function, e.g.
data Bool = False | True
foldBool :: r -- False constructor
-> r -- True constructor
-> Bool -> r
data Maybe a = Nothing | Just a
foldMaybe :: b -- Nothing constructor
-> (a -> b) -- Just constructor
-> Maybe a -> b
data List a = Empty | Cons a (List a)
foldList :: b -- Empty constructor
-> (a -> b -> b) -- Cons constructor
-> List a -> b
However, what's not clear to me is what happens if the same type constructor is used, but with a different type argument. E.g. instead of passing a List a
to Cons
, what about
data List a = Empty | Cons a (List (a,a))
Or, maybe a more crazy case:
data List a = Empty | Cons a (List (List a))
foldList :: b -- Empty constructor
-> ??? -- Cons constructor
-> List a -> b
The two plausible ideas I have for the ???
part are
(a -> b -> b)
, i.e. replacing all applications of theList
constructor recursively)(a -> List b -> b)
, i.e. merely replacing allList a
applications.
Which of the two would be correct - and why? Or would it be something different altogether?
This is a partial answer, only.
The issue the OP raises is: how to define
fold
/cata
in the case of non-regular recursive types?Since I don't trust myself in getting this right, I will resort to asking Coq instead. Let's start from a simple, regular recursive list type.
Nothing fancy here,
List A
is defined in terms ofList A
. (Remember this -- we'll get back to it.)What about the
cata
? Let's query the induction pinciple.Let's see. The above exploits dependent types:
P
depends on the actual value of the list. Let's just manually simplify it in the caseP list
is a constant typeB
. We obtain:which can be equivalently written as
Which is
foldr
except that the "current list" is also passed to the binary function argument -- not a major difference.Now, in Coq we can define a list in another subtly different way:
It looks the same type, but there is a profound difference. Here we are not defining the type
List A
in terms ofList A
. Rather, we are defining a type functionList2 : Type -> Type
in terms ofList2
. The main point of this is that the recursive references toList2
do not have to be applied toA
-- the fact that above we do so is only an incident.Anyway, let's see the type for the induction principle:
Let's remove the
List2 T
argument fromP
as we did before, basically assumingP
is constant on it.Equivalently rewritten:
Which roughly corresponds, in Haskell notation
Not so bad -- the base case now has to be a polymorphic function, much as
Empty
in Haskell is. It makes some sense. Similarly, the inductive case must be a polymorphic function, much asCons
is. There's an extraList2 a
argument, but we can ignore that, if we want.Now, the above is still a kind of fold/cata on a regular type. What about non regular ones? I will study
which in Coq becomes:
with induction principle:
Removing the "dependent" part:
In Haskell notation:
Aside from the additional
List3 (a, a)
argument, this is a kind of fold.Finally, what about the OP type?
Alas, Coq does not accept the type
since the inner
List4
occurrence is not in a strictly positive position. This is probably a hint that I should stop being lazy and using Coq to do the work, and start thinking about the involved F-algebras by myself... ;-)