Martijn van Steenbergen wrote:
Bonjour café,

data ExprF r
  =  Add  r  r
  |  Sub  r  r
  |  Mul  r  r
  |  Div  r  r
  |  Num  Int

This is a well-known pattern that for example allows nice notation of morphisms. But what is it called? I've heard fixed-point view, open datatypes and some others, but I'm curious where this pattern comes up in literature and what it is called there.

This is an example of "open recursion", which is when you take some recursive function/datatype and rewrite it without recursion by passing the function/type in as an argument to itself. It's the datatype equivalent of doing:

    fibF _ 0 = 0
    fibF _ 1 = 1
    fibF f n = f(n-1) + f(n-2)

    fib = fix fibF

Which can be useful for functions because we can use a different fixed-point operator, e.g. one that adds memoization abilities or other features in addition to the recursion.

As others've mentioned, the open-recursive version of a recursive data type happens to be a "functor". Or rather, the recursive type happens to be isomorphic to the least fixed point of a generating functor[1][2] because the functor is also, in the terms of recursion theory, an "initial algebra". Part of why this pattern is so nice comes from the fact that it's a functor (so we can use fmap to apply a function one ply down), but part of it also comes from the isomorphism of using an explicit fixed-point operator (which allows us to un-fix the type and do things like storing the accumulators of a fold directly in the normal constructors, rather than needing to come up with an ad-hoc isomorphic set of constructors[3]), and the fact that it's an initial algebra ties these two things together nicely.

This is also an example of Tim Sheard's "two-level types", albeit a trivial one since the fixed-point operator doesn't add anything other than recursion. One of the particular ideas behind Sheard's two-level types is that we can split the original recursive type in a different place where one of the levels contains some constructors and the other level contains other constructors. This can be helpful when you have a family (informally speaking) of similar types, as for example with implementing unification. All types that can be unified share constructors for unification variables; but the constructors for the structural components of the type are left up to another level. Thus we can reuse the variable processing code for unifying different types, and also be modular about the type being unified.



[1] This should be somewhat obvious if you're familiar with the inductive phrasing of constructing the set of all values for some type. E.g. "Basis: [] is a list. Induction: (:) takes a value and a list into a list". So we have some functor and we keep applying it over and over to generate the set of all values, building up from the base cases.

[2] Do note that in Haskell the least fixed point and the greatest fixed point coincide. Technically, whether the least or greatest fixed point is used depends on the construction (e.g. catamorphisms use least, anamorphisms use greatest). This is also related to the topic of "codata" which is the fixed point of a terminal coalgebra.

[3] Data.List.unfoldr is a prime example of an ad-hoc isomorphic set of constructors. Instead of the current type, we could instead use an implementation where:

    newtype Fix   f   = Fix { unFix :: f (Fix f) }
    data    ListF a r = Nil | Cons a r
    type    List  a   = Fix (ListF a)

    unfoldr :: (b -> ListF a b) -> b -> List a
    unfoldr = ...

which is a bit more obviously correlated with anamorphisms in recursion theory.

--
Live well,
~wren
_______________________________________________
Haskell-Cafe mailing list
Haskell-Cafe@haskell.org
http://www.haskell.org/mailman/listinfo/haskell-cafe

Reply via email to