Add FunctorFilter and TraverseFilter - #2405
Conversation
022ea7c to
23688dc
Compare
Codecov Report
@@ Coverage Diff @@
## master #2405 +/- ##
=========================================
+ Coverage 95.29% 95.39% +0.1%
=========================================
Files 351 357 +6
Lines 6371 6516 +145
Branches 286 288 +2
=========================================
+ Hits 6071 6216 +145
Misses 300 300
Continue to review full report at Codecov.
|
b0cd563 to
9c9d9de
Compare
895fcd8 to
f97ba06
Compare
|
|
||
| object FunctorEmpty { | ||
| implicit def catsFunctorForFunctorEmpty[F[_]](fe: FunctorEmpty[F]): Functor[F] = | ||
| fe.functor |
There was a problem hiding this comment.
This function here allows us to call things like FunctorEmpty[F].map, do you think this is worthwhile? I feel like this is an easy change that doesn't cause any ambiguities and let's us atleast have some of the niceties that we lose when doing composition instead of inheritance :)
There was a problem hiding this comment.
I don’t think I like it it’s an unusual use and only saving one .functor in there.
Alternatively could we makes functor provide this as a lower priority?
There was a problem hiding this comment.
I am not sure if it's needed either.
What would be the use case of it?
If user import FunctorEmpty._ and all of a sudden whenever there is a FunctorEmpty in scope there is also a Functor in scope, then will there be ambiguities with other possible Functor instances?
Should user be just writing the following instead?
def foo[F[_]: FunctorEmpty : Functor]: ....
There was a problem hiding this comment.
There won't be ambiguities, because only an explicit instance will be converted, notice it is (fe: FunctorEmpty[F]): Functor[F] and not (implicit fe: FunctorEmpty[F]): Functor[F].
The only real benefit is that when you have a method like this:
def foo[F[_]](fa: F[Int])(implicit F: FunctorEmpty[F]): F[String] =
F.map(fa)(_.toString) You'll be able to call map et al. directly on it instead of doing F.functor.map. So the benefit is rather small, but at the same time it doesn't cause ambiguities. Also note that import FunctorEmpty._ won't actually do anything as this instance will always be in scope whenever FunctorEmpty is in scope :)
There was a problem hiding this comment.
ah, sorry I missed that fe isn't implicit. As you said, the benefit is a bit small, especially since you usually don't need to write implicit F: FunctorEmpty[F] at the first place, you could just use context bound, so not much typing to be saved.
I'd prefer encouraging users to the more conventional type class syntax enabled style.
def foo[F[_]: FunctorEmpty : Functor](fa: F[Int]): F[String] = {
fa.map(_.toString).filter(_.length > 2)
}| checkAll("Order[Chain]", SerializableTests.serializable(Order[Chain[Int]])) | ||
|
|
||
| checkAll("Chain[Int]", TraverseEmptyTests[Chain].traverseEmpty[Int, Int, Int]) | ||
| checkAll("TraverseEmpty[Chain]", SerializableTests.serializable(TraverseEmpty[Chain])) |
There was a problem hiding this comment.
For some reason TraverseEmpty[Chain] seems not to be able to be serialized, though I really don't understand why. Any ideas?
I think that the issue here is that it was referencing an instance-level `catsDataInstancesForChain` from an abstract class. By changing it to reference `Chain.catsDataInstancesForChain`, it is a reference to a static member (and therefore doesn't actually need to be serialized). Take my explanation with a grain of salt -- like everyone else on the planet, I don't actually understand Java serialization. But at the end of the day it works :)
Make TraverseEmpty[Chain] serializable
|
|
||
| object FunctorEmpty { | ||
| implicit def catsFunctorForFunctorEmpty[F[_]](fe: FunctorEmpty[F]): Functor[F] = | ||
| fe.functor |
There was a problem hiding this comment.
I don’t think I like it it’s an unusual use and only saving one .functor in there.
Alternatively could we makes functor provide this as a lower priority?
| @typeclass | ||
| trait FunctorEmpty[F[_]] extends Serializable { | ||
| def functor: Functor[F] | ||
|
|
There was a problem hiding this comment.
Why doesn’t this have an empty method with the law that anything filtered out becomes empty?
Also, if you have a FunctorEmpty and MonoidK empty should be consistent.
There was a problem hiding this comment.
Can we define any extra methods if we require implementors to implement an empty method? Or just for the laws? The Haskell precedent doesn't seem to define a method like this and I'm not sure if we should either, need to think about what the actual benefits are 🤔
There was a problem hiding this comment.
well, I guess if you have an empty, since you can't combine it, you are always stuck with an empty. But still, that's interesting for laws empty.map(fn) == empty etc...
I hate to have a hacky way to make an empty, but not provide it (namely give me any instance, and I will filter it).
There was a problem hiding this comment.
Here's some discussion on it #1365 and here's the haskell counter part http://hackage.haskell.org/package/witherable-0.2/docs/Data-Witherable.html
There was a problem hiding this comment.
The original PR also has some discussion on why it wasn't included in the first place https://github.com/typelevel/cats/pull/1225/files#discussion_r71987653
There was a problem hiding this comment.
More specifically, this comment: #1225 (comment)
There was a problem hiding this comment.
Yeah after thinking about it more a bit, I think I'd vote to keep it as is without the empty method.
| trait TraverseEmpty[F[_]] extends FunctorEmpty[F] { | ||
| def traverse: Traverse[F] | ||
|
|
||
| override def functor: Functor[F] = traverse |
There was a problem hiding this comment.
I haven't spent a lot of time on the this type of encoding. Is there a reason that functor should ever not be the traverse? In other words, do we lose anything by making this final?
There was a problem hiding this comment.
Nope, I agree fully, I'll change it :)
|
I understand the arguments against providing |
|
@non I'm fine with renaming them back to |
4b65c9a to
bf07dd3
Compare
|
When I originally added these in #1225, I had some doctest examples. It would be nice to see those come back. Also is any of the compose work in that PR still relevant? |
|
Hi Cody, I'll add those doctests back :) |
ea4753d to
96e3cc8
Compare
96e3cc8 to
716d701
Compare
| (implicit G: Applicative[G]): G[EitherT[F, L, B]] = | ||
| G.map( | ||
| F0.traverseFilter[G, Either[L, A], Either[L, B]](fa.value) { | ||
| case l@Left(_) => G.pure(Option(l.rightCast[B])) |
There was a problem hiding this comment.
The Option.apply call here is either doing an unnecessary null-check here or is masking a null. I think that we should just use the Some constructor instead (and I believe that we have followed this convention elsewhere in Cats).
| (fa: EitherT[F, L, A]) | ||
| (f: A => G[Boolean]) | ||
| (implicit G: Applicative[G]): G[EitherT[F, L, A]] = | ||
| G.map(F0.filterA(fa.value)(_.fold(_ => G.pure(true), f)))(EitherT[F, L, A]) |
There was a problem hiding this comment.
Here and in the filter method, it isn't obvious to me that a Left should result in true as opposed to false. Is there a law that guides us in this direction? If not, was there a particular reason for this choice? I'm not saying that I think that it should be the other way around; just that I wouldn't have known which one to pick. And for the Nested instances, we use a Traverse for the outer type and a TraverseFilter for the inner type, so it seems a little weird to me that you couldn't get a TraverseFilter instance for an F[Either[L, ?]] but if you lift it into an EitherT you can get one. This makes me question the validity/consistency/intuitiveness of this instance.
There was a problem hiding this comment.
This is just the implementation we had in cats-mtl, so I'm not sure what the motivation was. I agree that it seems weird though, maybe we should delete it. Did you provide one in your initial PR?
There was a problem hiding this comment.
No, I didn't. I'd be inclined to leave it out until someone makes a good argument for it.
There was a problem hiding this comment.
Yeah agree, can always add it back later 👍
|
|
||
| def traverseFilter[G[_], A, B](fa: Option[A])(f: (A) => G[Option[B]])(implicit G: Applicative[G]): G[Option[B]] = | ||
| fa match { | ||
| case _: None.type => G.pure(Option.empty[B]) |
There was a problem hiding this comment.
Minor: I think that this could just be case None
|
|
||
| override def filterA[G[_], A](fa: Option[A])(f: (A) => G[Boolean])(implicit G: Applicative[G]): G[Option[A]] = | ||
| fa match { | ||
| case _: None.type => G.pure(Option.empty[A]) |
| OptionT(F.defer(fa.value)) | ||
| } | ||
|
|
||
| implicit def optionTFunctorFilter[F[_]: Functor]: FunctorFilter[OptionT[F, ?]] = { |
There was a problem hiding this comment.
There was a TraverseFilter instance for OptionT in #1225. Is there an issue with it?
There was a problem hiding this comment.
No idea, just took what was there from cats-mtl
There was a problem hiding this comment.
I'm having trouble tracking down the history in cats-mtl, but I think that we should have the OptionT instance. It should be have identically to the corresponding Nested instance.
|
@ceedubs I pushed a new commit, addressing your points :) |
| EitherT(F.defer(fa.value)) | ||
| } | ||
|
|
||
|
|
| implicit val G: TraverseFilter[G] = G0 | ||
| } | ||
|
|
||
|
|
568daf4 to
b4c5080
Compare
Should fix #2348. Needs to be coordinated with their removal from
cats-mtl:)