The catamorphism for Either is a generalisation of its fold. The catamorphism enables operations not available via fold.

This article is part of an article series about catamorphisms. A catamorphism is a universal abstraction that describes how to digest a data structure into a potentially more compact value.

This article presents the catamorphism for Either (also known as Result), as well as how to identify it. The beginning of this article presents the catamorphism in C#, with examples. The rest of the article describes how to deduce the catamorphism. This part of the article presents my work in Haskell. Readers not comfortable with Haskell can just read the first part, and consider the rest of the article as an optional appendix.

Either is a data container that models two mutually exclusive results. It's often used to return values that may be either correct (right), or incorrect (left). In statically typed functional programming with a preference for total functions, Either offers a saner, more reasonable way to model success and error results than throwing exceptions.

C# catamorphism #

This article uses the Church encoding of Either. The catamorphism for Either is the Match method:

T Match<T>(Func<LT> onLeft, Func<RT> onRight);

Until this article, all previous catamorphisms have been a pair made from an initial value and a function. The Either catamorphism is a generalisation, since it's a pair of functions. One function handles the case where there's a left value, and the other function handles the case where there's a right value. Both functions must return the same, unifying type, which is often a string or something similar that can be shown in a user interface:

> IEither<TimeSpanint> e = new Left<TimeSpanint>(TimeSpan.FromMinutes(3));
> e.Match(ts => ts.ToString(), i => i.ToString())
"00:03:00"
> IEither<TimeSpanint> e = new Right<TimeSpanint>(42);
> e.Match(ts => ts.ToString(), i => i.ToString())
"42"

You'll often see examples like the above that turns both left and right cases into strings or something that can be represented as a unifying response type, but this is in no way required. If you can come up with a unifying type, you can convert both cases to that type:

> IEither<Guidstring> e = new Left<Guidstring>(Guid.NewGuid());
> e.Match(g => g.ToString().Count(c => 'a' <= c), s => s.Length)
12
> IEither<Guidstring> e = new Right<Guidstring>("foo");
> e.Match(g => g.ToString().Count(c => 'a' <= c), s => s.Length)
3

In the two above examples, you use two different functions that both reduce respectively Guid and string values to a number. The function that turns Guid values into a number counts how many of the hexadecimal digits that are greater than or equal to A (10). The other function simply returns the length of the string, if it's there. That example makes little sense, but the Match method doesn't care about that.

In practical use, Either is often used for error handling. The article on the Church encoding of Either contains an example.

Either F-Algebra #

As in the previous article, I'll use Fix and cata as explained in Bartosz Milewski's excellent article on F-Algebras.

While F-Algebras and fixed points are mostly used for recursive data structures, you can also define an F-Algebra for a non-recursive data structure. You already saw an example of that in the articles about Boolean catamorphism and Maybe catamorphism. The difference between e.g. Maybe values and Either is that both cases of Either carry a value. You can model this as a Functor with both a carrier type and two type arguments for the data that Either may contain:

data EitherF l r c = LeftF l | RightF r deriving (ShowEqRead)
 
instance Functor (EitherF l r) where
  fmap _  (LeftF l) = LeftF l
  fmap _ (RightF r) = RightF r

I chose to call the 'data types' l (for left) and r (for right), and the carrier type c (for carrier). As was also the case with BoolF and MaybeF, the Functor instance ignores the map function because the carrier type is missing from both the LeftF case and the RightF case. Like the Functor instances for BoolF and MaybeF, it'd seem that nothing happens, but at the type level, this is still a translation from EitherF l r c to EitherF l r c1. Not much of a function, perhaps, but definitely an endofunctor.

As was also the case when deducing the Maybe and List catamorphisms, Haskell isn't too happy about defining instances for a type like Fix (EitherF l r). To address that problem, you can introduce a newtype wrapper:

newtype EitherFix l r = EitherFix { unEitherFix :: Fix (EitherF l r) } deriving (ShowEqRead)

You can define Functor, Applicative, Monad, etc. instances for this type without resorting to any funky GHC extensions. Keep in mind that ultimately, the purpose of all this code is just to figure out what the catamorphism looks like. This code isn't intended for actual use.

A pair of helper functions make it easier to define EitherFix values:

leftF :: l -> EitherFix l r
leftF = EitherFix . Fix . LeftF
 
rightF :: r -> EitherFix l r
rightF = EitherFix . Fix . RightF

With those functions, you can create EitherFix values:

Prelude Data.UUID Data.UUID.V4 Fix Either> leftF <$> nextRandom
EitherFix {unEitherFix = Fix (LeftF e65378c2-0d6e-47e0-8bcb-7cc29d185fad)}
Prelude Data.UUID Data.UUID.V4 Fix Either> rightF "foo"
EitherFix {unEitherFix = Fix (RightF "foo")}

That's all you need to identify the catamorphism.

Haskell catamorphism #

At this point, you have two out of three elements of an F-Algebra. You have an endofunctor (EitherF l r), and an object c, but you still need to find a morphism EitherF l r c -> c. Notice that the algebra you have to find is the function that reduces the functor to its carrier type c, not the 'data types' l and r. This takes some time to get used to, but that's how catamorphisms work. This doesn't mean, however, that you get to ignore l and r, as you'll see.

As in the previous articles, start by writing a function that will become the catamorphism, based on cata:

eitherF = cata alg . unEitherFix
  where alg  (LeftF l) = undefined
        alg (RightF r) = undefined

While this compiles, with its undefined implementations, it obviously doesn't do anything useful. I find, however, that it helps me think. How can you return a value of the type c from the LeftF case? You could pass an argument to the eitherF function:

eitherF fl = cata alg . unEitherFix
  where alg  (LeftF l) = fl l
        alg (RightF r) = undefined

While you could, technically, pass an argument of the type c to eitherF and then return that value from the LeftF case, that would mean that you would ignore the l value. This would be incorrect, so instead, make the argument a function and call it with l. Likewise, you can deal with the RightF case in the same way:

eitherF :: (l -> c) -> (r -> c) -> EitherFix l r -> c
eitherF fl fr = cata alg . unEitherFix
  where alg  (LeftF l) = fl l
        alg (RightF r) = fr r

This works. Since cata has the type Functor f => (f a -> a) -> Fix f -> a, that means that alg has the type f a -> a. In the case of EitherF, the compiler infers that the alg function has the type EitherF l r c -> c, which is just what you need!

You can now see what the carrier type c is for. It's the type that the algebra extracts, and thus the type that the catamorphism returns.

This, then, is the catamorphism for Either. As has been consistent so far, it's a pair, but now made from two functions. As you've seen repeatedly, this isn't the only possible catamorphism, since you can, for example, trivially flip the arguments to eitherF.

I've chosen the representation shown here because it's isomorphic to the either function from Haskell's built-in Data.Either module, which calls the function the "Case analysis for the Either type". Both of these functions (eitherF and either) are equivalent to the above C# Match method.

Basis #

You can implement most other useful functionality with eitherF. Here's the Bifunctor instance:

instance Bifunctor EitherFix where
  bimap f s = eitherF (leftF . f) (rightF . s)

From that instance, the Functor instance trivially follows:

instance Functor (EitherFix l) where
  fmap = second

On top of Functor you can add Applicative:

instance Applicative (EitherFix l) where
  pure = rightF
  f <*> x = eitherF leftF (<$> x) f

Notice that the <*> implementation is similar to to the <*> implementation for MaybeFix. The same is the case for the Monad instance:

instance Monad (EitherFix l) where
  x >>= f = eitherF leftF f x

Not only is EitherFix Foldable, it's Bifoldable:

instance Bifoldable EitherFix where
  bifoldMap = eitherF

Notice, interestingly, that bifoldMap is identical to eitherF.

The Bifoldable instance enables you to trivially implement the Foldable instance:

instance Foldable (EitherFix l) where
  foldMap = bifoldMap mempty

You may find the presence of mempty puzzling, since bifoldMap (or eitherF; they're identical) takes as arguments two functions. Is mempty a function?

Yes, mempty can be a function. Here, it is. There's a Monoid instance for any function a -> m, where m is a Monoid instance, and mempty is the identity for that monoid. That's the instance in use here.

Just as EitherFix is Bifoldable, it's also Bitraversable:

instance Bitraversable EitherFix where
  bitraverse fl fr = eitherF (fmap leftF . fl) (fmap rightF . fr)

You can comfortably implement the Traversable instance based on the Bitraversable instance:

instance Traversable (EitherFix l) where
  sequenceA = bisequenceA . first pure

Finally, you can implement conversions to and from the standard Either type, using ana as the dual of cata:

toEither :: EitherFix l r -> Either l r
toEither = eitherF Left Right
 
fromEither :: Either a b -> EitherFix a b
fromEither = EitherFix . ana coalg
  where coalg  (Left l) =  LeftF l
        coalg (Right r) = RightF r

This demonstrates that EitherFix is isomorphic to Either, which again establishes that eitherF and either are equivalent.

Relationships #

In this series, you've seen various examples of catamorphisms of structures that have no folds, catamorphisms that coincide with folds, and now a catamorphism that is more general than the fold. The introduction to the series included this diagram:

Catamorphisms and folds as sets, for various sum types.

This shows that Boolean values and Peano numbers have catamorphisms, but no folds, whereas for Maybe and List, the fold and the catamorphism is the same. For Either, however, the fold is a special case of the catamorphism. The fold for Either 'pretends' that the left side doesn't exist. Instead, the left value is interpreted as a missing right value. Thus, in order to fold Either values, you must supply a 'fallback' value that will be used in case an Either value isn't a right value:

Prelude Fix Either> e = rightF LT :: EitherFix Integer Ordering
Prelude Fix Either> foldr (const . show) "" e
"LT"
Prelude Fix Either> e = leftF 42 :: EitherFix Integer Ordering
Prelude Fix Either> foldr (const . show) "" e
""

In a GHCi session like the above, you can create two Either values of the same type. The right case is an Ordering value, while the left case is an Integer value.

With foldr, there's no way to access the left case. While you can access and transform the right Ordering value, the number 42 is simply ignored during the fold. Instead, the default value "" is returned.

Contrast this with the catamorphism, which can access both cases:

Prelude Fix Either> e = rightF LT :: EitherFix Integer Ordering
Prelude Fix Either> eitherF show show e
"LT"
Prelude Fix Either> e = leftF 42 :: EitherFix Integer Ordering
Prelude Fix Either> eitherF show show e
"42"

In a session like this, you recreate the same values, but using the catamorphism eitherF, you can now access and transform both the left and the right cases. In other words, the catamorphism enables you to perform operations not possible with the fold.

It's interesting, however, to note that while the fold is a specialisation of the catamorphism, the bifold is identical to the catamorphism.

Summary #

The catamorphism for Either is a pair of functions. One function transforms the left case, while the other function transforms the right case. For any Either value, only one of those functions will be used.

When I originally encountered the concept of a catamorphism, I found it difficult to distinguish between catamorphism and fold. My problem was, I think, that the tutorials I ran into mostly used linked lists to demonstrate how, in that case, the fold is the catamorphism. It turns out, however, that this isn't always the case. A catamorphism is a general abstraction. A fold, on the other hand, seems to me to be mostly related to collections.

In this article you saw the first example of a catamorphism that can do more than the fold. For Either, the fold is just a special case of the catamorphism. You also saw, however, how the catamorphism was identical to the bifold. Thus, it's still not entirely clear how these concepts relate. Therefore, in the next article, you'll get an example of a container where there's no bifold, and where the catamorphism is, indeed, a generalisation of the fold.

Next: Tree catamorphism.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 03 June 2019 06:05:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 03 June 2019 06:05:00 UTC