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 FunctorFilter and TraverseFilter #2405

Merged
merged 15 commits into from
Sep 3, 2018
Merged

Conversation

LukaJCB
Copy link
Member

@LukaJCB LukaJCB commented Aug 15, 2018

Should fix #2348. Needs to be coordinated with their removal from cats-mtl :)

@kailuowang kailuowang added this to the 1.3 milestone Aug 15, 2018
@codecov-io
Copy link

codecov-io commented Aug 15, 2018

Codecov Report

Merging #2405 into master will increase coverage by 0.1%.
The diff coverage is 100%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
core/src/main/scala/cats/data/Const.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/instances/list.scala 100% <100%> (ø) ⬆️
core/src/main/scala/cats/instances/option.scala 100% <100%> (ø) ⬆️
...ala/cats/laws/discipline/TraverseFilterTests.scala 100% <100%> (ø)
core/src/main/scala/cats/FunctorFilter.scala 100% <100%> (ø)
core/src/main/scala/cats/instances/map.scala 94.59% <100%> (+1.04%) ⬆️
core/src/main/scala/cats/data/OptionT.scala 96.9% <100%> (+0.39%) ⬆️
core/src/main/scala/cats/data/Nested.scala 95.12% <100%> (+0.67%) ⬆️
.../src/main/scala/cats/laws/TraverseFilterLaws.scala 100% <100%> (ø)
...s/src/main/scala/cats/laws/FunctorFilterLaws.scala 100% <100%> (ø)
... and 12 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 77c9319...b4c5080. Read the comment docs.

@LukaJCB LukaJCB changed the title Add FunctorEmpty and TraverseEmpty WIP: Add FunctorEmpty and TraverseEmpty Aug 15, 2018
@LukaJCB LukaJCB force-pushed the filter-classes branch 2 times, most recently from b0cd563 to 9c9d9de Compare August 15, 2018 16:44
@LukaJCB LukaJCB changed the title WIP: Add FunctorEmpty and TraverseEmpty Add FunctorEmpty and TraverseEmpty Aug 15, 2018

object FunctorEmpty {
implicit def catsFunctorForFunctorEmpty[F[_]](fe: FunctorEmpty[F]): Functor[F] =
fe.functor
Copy link
Member Author

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 :)

Copy link
Contributor

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?

Copy link
Contributor

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]: ....

Copy link
Member Author

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 :)

Copy link
Contributor

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)
}

Copy link
Member Author

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]))
Copy link
Member Author

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?

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 that I got it -- LukaJCB#4

ceedubs and others added 2 commits August 16, 2018 07:39
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 :)

object FunctorEmpty {
implicit def catsFunctorForFunctorEmpty[F[_]](fe: FunctorEmpty[F]): Functor[F] =
fe.functor
Copy link
Contributor

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]

Copy link
Contributor

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.

Copy link
Member Author

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 🤔

Copy link
Contributor

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).

Copy link
Member Author

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

Copy link
Member Author

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

Copy link
Contributor

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)

Copy link
Member Author

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
Copy link
Member

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?

Copy link
Member Author

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 :)

@non
Copy link
Contributor

non commented Aug 28, 2018

I understand the arguments against providing empty -- but given that what we have is a functor that can be filtered, I'd prefer a name like FunctorFilter or FilterableFunctor or something similar. I think mentioning empty in the name is not a good idea if we don't want to require that method.

@LukaJCB
Copy link
Member Author

LukaJCB commented Aug 28, 2018

@non I'm fine with renaming them back to TraverseFilter and FunctorFilter as they were originally called. :)

@LukaJCB LukaJCB changed the title Add FunctorEmpty and TraverseEmpty Add FunctorFilter and TraverseFilter Aug 28, 2018
@ceedubs
Copy link
Contributor

ceedubs commented Aug 31, 2018

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?

@LukaJCB
Copy link
Member Author

LukaJCB commented Sep 2, 2018

Hi Cody, I'll add those doctests back :)
W.r.t to compose, I just added instances for Nested, not sure if we also need the explicit compose 🤔

(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]))
Copy link
Contributor

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])
Copy link
Contributor

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.

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Member Author

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])
Copy link
Contributor

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])
Copy link
Contributor

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, ?]] = {
Copy link
Contributor

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?

Copy link
Member Author

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

Copy link
Contributor

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.

@LukaJCB
Copy link
Member Author

LukaJCB commented Sep 2, 2018

@ceedubs I pushed a new commit, addressing your points :)

ceedubs
ceedubs previously approved these changes Sep 2, 2018
Copy link
Contributor

@ceedubs ceedubs left a 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))
}


Copy link
Contributor

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
}


Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: extra newline here.

@LukaJCB LukaJCB merged commit 2f129c6 into typelevel:master Sep 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Move FunctorEmpty and TraverseEmpty back into cats-core?
7 participants