Bimonad

API Documentation: Bimonad

The Bimonad trait directly extends Monad and Comonad without introducing new methods. Bimonad is different from other Bi typeclasses like Bifunctor, Bifoldable or Bitraverse where the prefix describes a F[_, _]. The Bimonad is a F[_] and the Bi prefix has a different meaning here: it's both a Monad and a Comonad.
Keep in mind Bimonad has its own added laws so something that is both monadic and comonadic may not necessarily be a lawful Bimonad.

If you use Bimonad as a convenience type such that:

def f[T[_]: Monad: Comonad, S](fa: T[S]): S

is re-written to:

def f[T[_]: Bimonad, S](fa: T[S]): S

then T[_] also needs to respect an extra set of laws.

NonEmptyList as a Bimonad

NonEmptyList[_] is a lawful Bimonad so you can chain computations (like a Monad) and extract the result at the end (like a Comonad).

Here is a possible implementation:

import cats._
import cats.data._
import cats.syntax.all._

implicit val nelBimonad: Bimonad[NonEmptyList] =
  new Bimonad[NonEmptyList] {

    // in order to have a lawful bimonad `pure` and `extract` need to respect: `nelBimonad.extract(nelBimonad.pure(a)) <-> a`
    override def pure[A](a: A): NonEmptyList[A] =
      NonEmptyList.one(a)

    override def extract[A](fa: NonEmptyList[A]): A =
      fa.head

    // use coflatMap from NonEmptyList
    override def coflatMap[A, B](fa: NonEmptyList[A])(f: NonEmptyList[A] => B): NonEmptyList[B] =
      fa.coflatMap(f)

    // use flatMap from NonEmptyList
    override def flatMap[A, B](fa: NonEmptyList[A])(f: A => NonEmptyList[B]): NonEmptyList[B] =
      fa.flatMap(f)

    // the tailRecM implementation is not the subject of this material
    // as an exercise try to implement it yourself
    override def tailRecM[A, B](a: A)(fn: A => NonEmptyList[Either[A, B]]): NonEmptyList[B] =
      ???
  }
// nelBimonad: Bimonad[NonEmptyList] = repl.MdocSession$MdocApp$$anon$1@f10001e

Note the equivalence:

nelBimonad.pure(true).extract === NonEmptyList.one(true).head
// res0: Boolean = true

Using generic bimonad syntax we could define a function that appends and extracts a configuration:

def make[T[_]: Bimonad](config: T[String]): String = 
  config
    .flatMap(c => Bimonad[T].pure(c + " with option A"))
    .flatMap(c => Bimonad[T].pure(c + " with option B"))
    .flatMap(c => Bimonad[T].pure(c + " with option C"))
    .extract

This works with one element non-empty lists:

make(NonEmptyList.one("config"))
// res1: String = "config with option A with option B with option C"

Function0[_] and Eval[_] are also lawful bimonads so the following calls are also valid:

make(() => "config")
// res2: String = "config with option A with option B with option C"

make(Eval.later("config"))
// res3: String = "config with option A with option B with option C"