-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Conversation
|
||
The `Bimonad` trait directly extends `Monad` and `Comonad` without introducing new behaviour. `Bimonad` is | ||
different of other `Bi` typeclasses like `Bifunctor`, `Bifoldable` or `Bitraverse` where the prefix describes a | ||
`F[_, _]`. The `Bimonad` is a `F[_]` and could be better seen as a dual monad i.e. something that is both a `Monad` and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"dual monad" is another one that I worry is confusing. For one, Comonad
is not a Monad
. Actually it is the dual of Monad
!
https://typelevel.org/cats/typeclasses/comonad.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DualMonad
\SelfDualMonad
thing i picked from old issues like #1297
I'll change it to:
The `Bimonad` is a `F[_]` and the `Bi` prefix has a different meaning here: it's both a `Monad` and a `Comonad`.
import cats.data._ | ||
import cats.implicits._ | ||
|
||
Bimonad[Eval].pure(true).extract === Eval.now(true).value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what you are trying to show here is that pure
is implemented as now
and extract
as value
? One alternative would be to define Bimonad[Eval]
here just for demonstrative purposes, WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what you are trying to show here is that
pure
is implemented asnow
andextract
asvalue
?
Yes
One alternative would be to define Bimonad[Eval] here just for demonstrative purposes, WDYT?
Something like that ?
val evalBimonad = Bimonad[Eval]
evalBimonad.pure(true).extract === Eval.now(true).value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking like how they define optionMonad
in the Monad
docs:
http://typelevel.org/cats/typeclasses/monad.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution! :)
import cats.implicits._ | ||
|
||
implicit def evalBimonad(implicit monad: Monad[Eval], comonad: Comonad[Eval]) = | ||
new Bimonad[Eval] { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 the coflatMap from the Eval comonad | ||
override def coflatMap[A, B](fa: Eval[A])(f: Eval[A] => B): Eval[B] = | ||
//use the coflatMap from the NonEmptyList comonad | ||
override def coflatMap[A, B](fa: NonEmptyList[A])(f: NonEmptyList[A] => B): NonEmptyList[B] = | ||
comonad.coflatMap(fa)(f) |
There was a problem hiding this comment.
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
:)
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
.
…NEL `flatMap` and `coflatMap`
### 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 based on existing monad and comonad: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a possible implementation based on existing monad and comonad: | |
Here is a possible implementation: |
implicit def nelBimonad = | ||
new Bimonad[NonEmptyList] { | ||
|
||
//use NonEmptyList specific methods for creation and extraction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a good place to point out the Bimonad
law about the relationship between pure
and extract
? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
//in order to have a lawful bimonad `pure` and `extract` need to respect: `nelBimonad.extract(nelBimonad.pure(a)) <-> a`
Something like this ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for iterating on all the feedback! Looking good, just a couple tiny things :)
implicit def nelBimonad = | ||
new Bimonad[NonEmptyList] { | ||
|
||
//in order to have a lawful bimonad `pure` and `extract` need to respect: `nelBimonad.extract(nelBimonad.pure(a)) <-> a` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a personal style nitpick but I prefer a space here between //
and the comment 😅
//in order to have a lawful bimonad `pure` and `extract` need to respect: `nelBimonad.extract(nelBimonad.pure(a)) <-> a` | |
// in order to have a lawful bimonad `pure` and `extract` need to respect: `nelBimonad.extract(nelBimonad.pure(a)) <-> a` |
keep in mind `Bimonad` has its own added laws so something that is both monadic | ||
and comonadic may not necessarily be a lawful `Bimonad`. |
There was a problem hiding this comment.
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 ...
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`. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... like this:
Keep in mind `Bimonad` has its own added laws so something that is both monadic | |
and comonadic may not necessarily be a lawful `Bimonad`. | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your contribution! :)
@armanbilge is this ready to merge or do i need 2 reviews ? I want to start working on another doc for |
Another pair of eyes is always good, maybe @johnynek can take another look at the next opportunity :) |
|
||
If you use `Bimonad` as a convenience type such that: | ||
```scala | ||
def f[T[_] : Monad, Comonad, S](fa: T[S]): S |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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]
import cats.data._ | ||
import cats.implicits._ | ||
|
||
implicit def nelBimonad = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would make this a implicit val
since it doesn't have any args.
Thanks! |
The first draft for
Bimonad
docs as required in #1801Couldn't find a better example .. i'm open to suggestions.