Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation for Bimonad #4076

Merged
merged 11 commits into from
Dec 24, 2021
85 changes: 85 additions & 0 deletions docs/src/main/mdoc/typeclasses/bimonad.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
layout: docs
title: "Bimonad"
section: "typeclasses"
source: "core/src/main/scala/cats/Bimonad.scala"
scaladoc: "#cats.Bimonad"
---
# 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`.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... like this:

Suggested change
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:
```scala
def f[T[_] : Monad, Comonad, S](fa: T[S]): S
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be def f[T[_]: Monad: Comonad, S](fa: T[S]): S

I think the comma here is makingComonad just a type parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep my bad ..:Monad :Comonad is what I wanted

```
is re-written to:
```scala
def f[T[_] : Bimonad, S](fa: T[S]): S
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we generally don't put a space after the type, so we would change to: f[T[_]: Bimonad, S]

```
keep in mind `Bimonad` has its own added laws so something that is both monadic
and comonadic may not necessarily be a lawful `Bimonad`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is important enough to include at the beginning ...


###Eval as a Bimonad
Eval 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 based on existing monad and comonad:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Here is a possible implementation based on existing monad and comonad:
Here is a possible implementation:

```scala mdoc
import cats._
import cats.data._
import cats.implicits._

implicit def evalBimonad(implicit monad: Monad[Eval], comonad: Comonad[Eval]) =
new Bimonad[Eval] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eval[_] is a bit of a bad choice here because the Comonad is not very safe. Calling .value on an Eval must be done with some care (it needs to be at O(1) stack depth or you lose the stack safety. If you are calling this as part of a recursive call it often blows up).

Id[_] is also a Comonad, but a bit of a boring one. I wonder if NonEmptyList[_] would be a better example since it is non-trivial but also not somewhat unsafe in this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm ok, I was hesitating between Eval[_] and NonEmptyList[_] for the main example.
I'll just use NonEmptyList[_].


//use Eval specific methods for creation and extraction
override def pure[A](a: A): Eval[A] =
Eval.now(a)

override def extract[A](x: Eval[A]): A =
x.value

//use the coflatMap from the Eval comonad
override def coflatMap[A, B](fa: Eval[A])(f: Eval[A] => B): Eval[B] =
comonad.coflatMap(fa)(f)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually we can directly call flatMap and coflatMap directly on the NonEmptyList :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hopping this shows flatMap and coflatMap as originating from monad and comonad.
Don't you think it's a good idea to delegate to the monad and comonad variables ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I do see that now. But then isn't it confusing, because pure and extract are also defined in monad and comonad, so we should delegate those as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.
I thought using one and head in the implementation will make this more obvious:

nelBimonad.pure(true).extract === NonEmptyList.one(true).head

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought using one and head in the implementation will make this more obvious

I agree, I think this is the right way to do it. So, we can just delegate flatMap and coflatMap to the list without monad/comonad and maybe leave tailRecM as an exercise for the reader 😉

Anyway just my opinion, not really sure what the best thing to do here is. But I think separating the implementations of pure and extract makes it seem like they are inconsistent or different from the definitions in monad or comonad.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the current tailRecM implementation for NonEmptyList[_] doesn't look trivial .. I mean I fear Its understanding detours too much and I agree it's a good idea to leave it as an exercise 👍

Anyway just my opinion, not really sure what the best thing to do here is. But I think separating the implementations of pure and extract makes it seem like they are inconsistent or different from the definitions in monad or comonad.

Yes, I will remove monad and comonad.


//use the flatMap and tailRecM from the Eval monad
override def flatMap[A, B](fa: Eval[A])(f: A => Eval[B]): Eval[B] =
monad.flatMap(fa)(f)

override def tailRecM[A, B](a: A)(f: A => Eval[Either[A, B]]): Eval[B] =
monad.tailRecM(a)(f)
}
```

Note the equivalence:
```scala mdoc
evalBimonad.pure(true).extract === Eval.now(true).value
```

Using generic bimonad syntax we could define a function that appends and extracts a configuration:
```scala mdoc
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 will work with all types of `Eval`:
```scala mdoc
make(Eval.now("config"))

make(Eval.later("config"))
```

`Function0` and `NonEmptyList` are also lawful bimonads so the following calls are also valid:
```scala mdoc
make(() => "config")

make(NonEmptyList.one("config"))
```
28 changes: 14 additions & 14 deletions docs/src/main/mdoc/typeclasses/typeclasses.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,20 +238,20 @@ From [cats-infographic by @tpolecat](https://github.com/tpolecat/cats-infographi
Originally from [@alexknvl](https://gist.github.com/alexknvl/d63508ddb6a728015ace53cb70a1fd5d)


| Type | Functor | Apply | Applicative | Monad | MonoidK | ApplicativeError | MonadError | CoflatMap | Comonad |
| --------------- |:-------:|:-----------------:|:-----------:|:-----:|:-------:|:-----------------:|:----------:|:---------:|:-------:|
| Id[A] | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✗ | ✔ | ✔ |
| Eval[A] | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✗ | ✔ | ✔ |
| Option[A] | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✗ |
| Const[K, A] | ✔ | ✔ (`K:Monoid`) | ✔ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Either[E, A] | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ |
| List[A] | ✔ | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✔ | ✗ |
| NonEmptyList[A] | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✗ | ✔ | ✔ |
| Stream[A] | ✔ | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✔ | ✗ |
| Map[K, A] | ✔ | ✔ | ✗ | ✗ | ✔ | ✗ | ✗ | ✗ | ✗ |
| Validated[E, A] | ✔ | ✔ (`E: Semigroup`)| ✔ | ✗ | ✗ | ✔ (`E: Semigroup`)| ✗ | ✗ | ✗ |
| Reader[E, A] | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Writer[E, A] | ✔ | ✔ (`E:Monoid`) | ✔ | ✔ | ✗ | ✗ | ✗ | ✔ | ✗ |
| Type | Functor | Apply | Applicative | Monad | MonoidK | ApplicativeError | MonadError | CoflatMap | Comonad | Bimonad |
| --------------- |:-------:|:-----------------:|:-----------:|:-----:|:-------:|:-----------------:|:----------:|:---------:|:-------:|:-------:|
| Id[A] | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✗ | ✔ | ✔ |✔ |
| Eval[A] | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✗ | ✔ | ✔ |✔ |
| Option[A] | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✗ |✗ |
| Const[K, A] | ✔ | ✔ (`K:Monoid`) | ✔ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |✗ |
| Either[E, A] | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ |✗ |
| List[A] | ✔ | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✔ | ✗ |✗ |
| NonEmptyList[A] | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✗ | ✔ | ✔ |✔ |
| Stream[A] | ✔ | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✔ | ✗ |✗ |
| Map[K, A] | ✔ | ✔ | ✗ | ✗ | ✔ | ✗ | ✗ | ✗ | ✗ |✗ |
| Validated[E, A] | ✔ | ✔ (`E: Semigroup`)| ✔ | ✗ | ✗ | ✔ (`E: Semigroup`)| ✗ | ✗ | ✗ |✗ |
| Reader[E, A] | ✔ | ✔ | ✔ | ✔ | ✗ | ✗ | ✗ | ✗ | ✗ |✗ |
| Writer[E, A] | ✔ | ✔ (`E:Monoid`) | ✔ | ✔ | ✗ | ✗ | ✗ | ✔ | ✗ |✗ |



Expand Down