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@6f718633
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"