After studying monads, a natural topic to turn to next is the comonad. A Comonad is a typeclass with the following methods:

trait Comonad[F[_]] extends Functor[F] { self =>
  def cobind[A, B](fa: F[A])(f: F[A] => B): F[B]
  def copoint[A](p: F[A]): A
}

A Comonad must satisfy the laws:

cobind(fa)(copoint) === fa // left identity
copoint(cobind(fa)(f)) === f(fa) // right identity

For comparison, a Monad looks similar, but with the location of the F[_] reversed:

trait Monad[F[_]] extends Functor[F] { self =>
  def bind[A,B](fa: F[A])(f: A => F[B]): F[B]
  def point[A](a: A): F[A]
}

Example of a Monad and Comonad - List vs NonEmptyList

A comonad is best understood via example. Recall that the standard example of a Monad is a list. The bind operation is flatMap:

def f(x: Int) = List(x, x+1, x+2)
val a = List(1,2,3)

M.point(a) === List(a)
M.bind(a)(f) === List(List(1,2,3),List(2,3,4), List(4,5,6)).flatten
             === List(1,2,3,2,3,4,4,5,6)

(The intermediate step above is displayed for clarity purposes.)

The corresponding example of a Comonad is the NonEmptyList (see code on github) - a list which is guaranteed to have at least one element. The code example above works as follows:

def f(x: NonEmptyList[Int]) = x.sum
val a = NonEmptyList(1,2,3)

M.copoint(a) === 1 //take out the first element
M.cobind(a)(f) === NonEmptyList(f(NonEmptyList(1,2,3)), f(NonEmptyList(2,3)), f(NonEmptyList(3)))
               === NonEmptyList(6,5,3)

So if we think of F as a container, we can think of copoint as pulling the "first" object out of the container, whereas cobind first "un-flattens" the container and then applies f to each unflattened-element.

How the laws make life interesting

After playing around a bit, I came up with the following possible implementation of cobind:

  def pullIn[A](puller: F[_])(pullee: A): F[A] = {
    def constFunc(x: Any) = pullee
    self.map(puller)(constFunc)
  }

  def cobindBoring[A, B](fa: F[A])(f: F[A] => B): F[B] = pullIn(fa)(f(fa))

This certainly satisfies the type signature. But it's quite boring - I can put cobindBoring onto Functor if I want to. Given the fancy name, a Comonad should be more interesting - turns out it is. This boring version of cobind fails the identity law:

> cobindBoring(NonEmptyList(1,2,3))(copoint)
NonEmptyList(1,1,1)

The result should be NonEmptyList(1,2,3). So the Comonad laws force our comonads to be interesting - at the very least, we need more than just any old implementation.

Strong Monads and Costrong Comonads

A strong monad is a monad with an additional function:

trait Strong {
  def strengthen[A,B](fa: (A,F[B])): F[(A,B)]
}

There are 4 laws a Strong Monad must satisfy - the wikipedia page displays them as commutative diagrams. A Costrong Comonad has a complementary definition:

trait Costrong {
  def costrengthen[A,B](fa: F[Either[A,B]]): Either[A,F[B]]
}

What this definition means is that we can co-strengthen the type of fa - it's either an A or else it's an F[B].

One computational way to interpret this is that the result is Either a result of type A, or else it's an error wrapped in F[B].

The laws a Comonad must satisfy can be derived by looking at the commutative diagrams on wikipedia and reversing the arrows. When doing this process, the product type (A,B) is replaced by the sum type Either[A,B]. The basic laws of a CostrongComonad are (in addition to the usual Comonad laws):

costrengthen(self.map(fb)(x => Right(x): Either[A,B])) === Right(fb)
costrengthen(fab).bimap(x=>x, copoint _) === copoint(fab)

There are also a couple of more complicated laws about associativity:

costrengthen(fab).bimap(x => x, cojoin _) === costrengthen(self.map(self.cojoin(fab))(costrengthen _))
eassoc(costrengthen(fabc).bimap(a => a, costrengthen _)) === costrengthen( self.map(fabc)(eassoc) )

where

def eassoc[A,B,C](eabc: Either[A,Either[B,C]]): Either[Either[A,B], C] = eabc match {
  case Left(a) => Left(Left(a))
  case Right(Left(b)) => Left(Right(b))
  case Right(Right(c)) => Right(c)
}

In one of the few posts I've seen discussing the topic, Edward Kmett shows that in the category Hask, all monads are strong and thus strength is boring. When trying to implement Costrong, I discovered that costrong comonads are also boring.

The reason why is very simple - every Comonad which is also a Functor also satisfies Costrong:

implicit def CostrongOfComonad[F[_]](m: Comonad[F]): Costrong[F] = new Costrong {
  def costrengthen[A,B](fa: F[Either[A,B]]): Either[A,F[B]] = fa match {
    case Left(a) => Left(a)
    case Right(b) => Right( m.map(fa)( eab => eab.bimap(_ => b, x => x)) )
  }
}

In Hask and Scal, of course, every Comonad is also a Functor.

In terms of NonEmptyList, what does this look like?

costrengthen(NonEmptyList(Left("oops"), Right(1), Right(2))) === Left("oops")
costrengthen(NonEmptyList(Right(0), Left("oops"), Right(2))) === Right(NonEmptyList(0, 0, 2))

It essentially takes a container, looks at the copoint of the container, and replaces all Left(...) instances by the copoint while simply unwrapping the Right elements.

The only properties I used when creating the Costrong object were Comonad.copoint and Functor.map - no additional work is necessary.

costrengthen is not unique

Consider another way of achieving a similar result. Rather than replacing the Left(...) instances with b, we might simply remove them:

However, the costrengthen function is not unique. Another possible definition for NonEmptyList:

def costrengthen[A,B](fab: NonEmptyList[Either[A,B]]): Either[A, NonEmptyList[B]] = copoint(fab) match {
  case Left(a) => Left(a)
  case Right(b) => Right({
    val filtered = fab.list.flatMap( _ match {
      case Left(_) => List[B]()
      case Right(bb) => List(bb)
    })
    new NonEmptyList(b, filtered.tail)
  })
}

Running tests shows that this definition also satisfies the CostrongComonad laws.

Is there some interesting definition of SemiCostrongComonad?

In his post on strength, Edward Kmett poses a different definition of costrengthen. He proposes that if there are any left elements in the container, return one of them. Otherwise, return the container after unwrapping the right elements.

I like this definition. But unfortunately it fails the second CostrongComonad law:

costrengthen(fab).bimap(x=>x, copoint _) === copoint(fab)

The counterexample is easy to see:

val x = NonEmptyList(Right(1), Left("oops"))
costrengthen(x).bimap(x => x, copoint _) === Left("oops").bimap(x => x, copoint _)
                                         === Left("oops")

copoint(x) === Right(1)

But here is a puzzle I'm posing: is there an alternate category theoretical construct, which I'll label SemiCostrongComonad, which is more interesting? Some definition which Edward Kmett's proposed costrengthen will actually satisfy?

Conclusion

In programming languages like Scala and Haskell, Costrong is a boring property for a Comonad to posess. Any Functor, which includes every Comonad satisfies this property. So if we are looking for interesting behaviors to add to a library like scalaz or the Haskell prelude, Costrength is not it.

Anyone who wants to play with these concepts can find my code on github.


Subscribe to the mailing list


Comments

comments powered by Disqus