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 theListconstructor recursively)(a -> List b -> b), i.e. merely replacing allList aapplications.
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/catain 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 Ais 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:
Pdepends on the actual value of the list. Let's just manually simplify it in the caseP listis a constant typeB. We obtain:which can be equivalently written as
Which is
foldrexcept 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 Ain terms ofList A. Rather, we are defining a type functionList2 : Type -> Typein terms ofList2. The main point of this is that the recursive references toList2do 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 Targument fromPas we did before, basically assumingPis 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
Emptyin Haskell is. It makes some sense. Similarly, the inductive case must be a polymorphic function, much asConsis. There's an extraList2 aargument, 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
List4occurrence 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... ;-)