In this article about the Free Monads in Haskell we are given a Toy datatype defined by:
data Toy b next =
Output b next
| Bell next
| Done
Fix is defined as follows:
data Fix f = Fix (f (Fix f))
Which allows to nest Toy expressions by preserving a common type:
Fix (Output 'A' (Fix Done)) :: Fix (Toy Char)
Fix (Bell (Fix (Output 'A' (Fix Done)))) :: Fix (Toy Char)
I understand how fixed points work for regular functions but I'm failing to see how the types are reduced in here. Which are the steps the compiler follows to evaluate the type of the expressions?
I'll make a more familiar, simpler type using
Fix
to see if you'll understand it.Here's the list type in a normal recursive definition:
Now, thinking back at how we use
fix
for functions, we know that we have to pass the function to itself as an argument. In fact, sinceList
is recursive, we can write a simpler nonrecursive datatype like so:Can you see how this is similar to, say, the function
f a recur = 1 + recur a
? In the same way thatfix
would passf
as an argument to itself,Fix
passesCons
as an argument to itself. Let's inspect the definitions offix
andFix
side-by-side:If you ignore the fluff of the constructor names and so on, you'll see that these are essentially exactly the same definition!
For the example of the
Toy
datatype, one could just define it recursively like so:However, we could use
Fix
to pass itself into itself, replacing all instances ofToy a
with a second type parameter:so, we can then just use
Fix (ToyStep a)
, which will be equivalent toToy a
, albeit in a different form. In fact, let's demonstrate them to be equivalent:You might be wondering, "Why do this?" Well usually, there's not much reason to do this. However, defining these sort of simplified versions of datatypes actually allow you to make quite expressive functions. Here's an example:
This is really an incredible function! It surprised me when I first saw it! Here's an example using the
Cons
datatype we made earlier, assuming we made aFunctor
instance:This essentially is
fix
for free, given that we useFix
on whatever datatype we use!Let's now imagine a function, given that
ToyStep a
is a functor instance, that simply collects all theOutputS
s into a list, like so:This is the power of using
Fix
rather than having your own datatype: generality.