-
-
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)
}
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.
Sounds good to me :)
@@ -25,6 +25,9 @@ class ChainSuite extends CatsSuite { | |||
checkAll("Chain[Int]", OrderTests[Chain[Int]].order) | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For some reason TraverseEmpty[Chain]
seems not to be able to be serialized, though I really don't understand why. Any ideas?
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 that I got it -- LukaJCB#4
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More specifically, this comment: #1225 (comment)
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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]) |
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.
Same comment as above
@@ -234,6 +234,20 @@ private[data] sealed abstract class OptionTInstances extends OptionTInstances0 { | |||
def defer[A](fa: => OptionT[F, A]): OptionT[F, A] = | |||
OptionT(F.defer(fa.value)) | |||
} | |||
|
|||
implicit def optionTFunctorFilter[F[_]: Functor]: FunctorFilter[OptionT[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.
There was a TraverseFilter
instance for OptionT
in #1225. Is there an issue with it?
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.
No idea, just took what was there from cats-mtl
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'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 :) |
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.
Glad to see these returning. Thanks @LukaJCB!
@@ -514,6 +514,8 @@ private[data] abstract class EitherTInstances extends EitherTInstances1 { | |||
def defer[A](fa: => EitherT[F, L, A]): EitherT[F, L, A] = | |||
EitherT(F.defer(fa.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.
Minor: extra newline here
implicit val G: TraverseFilter[G] = G0 | ||
} | ||
|
||
|
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.
Minor: extra newline here.
568daf4
to
b4c5080
Compare
Should fix #2348. Needs to be coordinated with their removal from
cats-mtl
:)