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

TraverseFilter (AKA Witherable) type class #1148

Closed
wants to merge 8 commits into from

Conversation

ceedubs
Copy link
Contributor

@ceedubs ceedubs commented Jun 19, 2016

This is not meant to be merged in its current form.

This is a proof of concept for adding a TraverseFilter type class, which is a port of Haskell's Data.Witherable. I'm not set on any of the naming, so feel free to offer other recommendations.

Would people be interested in something like this? Some things that I like about it are:

  • It provides filterM (actually a more general version of it) filterM broken? #1054.
  • It provides collect and mapFilter (aka mapMaybe) methods collect and mapMaybe for MonadFilter? #842. Since collect is often used on standard library collections, I think this is a nice feature to support.
  • traverseFilter supports operations that essentially do a filter and a traverse at the same time, which is something that you can't get from either Traverse or MonadFilter.

Some things that I'm not so sure about:

  • There's definitely some overlap with MonadFilter here. For example, filter and collect can be defined for both. I'm not sure if there should be a closer relationship between the two. Also syntax enrichment for methods like filter could potentially be ambiguous between the two types (though I don't know how much of a problem this will be in practice as most concrete types that support filter will have it defined on them).
  • (edit: I think this is resolved) I'm not so sure about the composition law. The Haskell wiki defines it as Compose . fmap (wither f) . wither g ≡ wither (Compose . fmap (wither f) . g), but I can't get the types to line up on that. I don't know Haskell very well and struggle with point free style, so I'm probably losing something in translation. I have a different version of the right hand side of the composition law that compiles, but it seems much less direct than the haskell version, and I don't know know if it's meaningfully distinct from the left hand side. I'm hoping someone who knows Haskell well (such as @aaronlevin or @sellout) and/or someone who knows laws well (such as @fthomas) can weigh in on this.

If people want to go forward with this I need to provide more instances, tests, and documentation.

* res0: List[String] = List(one, three)
* }}}
*/
def mapOptionA[G[_]: Applicative, A, B](fa: F[A])(f: A => G[Option[B]]): G[F[B]]
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 guess another potential name for this would be traverseOption.

@codecov-io
Copy link

codecov-io commented Jun 19, 2016

Current coverage is 89.32%

Merging #1148 into master will increase coverage by 0.52%

@@             master      #1148   diff @@
==========================================
  Files           232        238     +6   
  Lines          3073       3195   +122   
  Methods        3021       3139   +118   
  Messages          0          0          
  Branches         49         53     +4   
==========================================
+ Hits           2729       2854   +125   
+ Misses          344        341     -3   
  Partials          0          0          

Sunburst

Powered by Codecov. Last updated by 89bc41f...6f7840c

val rhs: Nested[M, N, F[C]] = fa.mapOptionA[Nested[M, N, ?], C](a =>
Nested(f(a).map(_.traverseM(g))))
lhs <-> rhs
}
Copy link
Contributor

Choose a reason for hiding this comment

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

So, I haven’t tried it, but the only difference I see between your law and the Haskell one is that the Haskell uses mapOptionA where you use traverseM. And I think the types line up if you make that change.

More superficially, the Haskell version also swaps the names f and g.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@sellout It's an Option[B] that I'm calling traverseM on. I think there's an extra layer of Option if I try to use mapOptionA. It seems off since I'm not actually calling mapOptionA twice.

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 it’s fine that it’s an Option[B] – the type signatures of mapOptionA and traverseM are both F[A] =...> G[F[B]]

def mapOptionA[G[_]: Applicative, A, B](fa: F[A])(f: A => G[Option[B]]): G[F[B]]
def traverseM[G[_]: Applicative, A, B](fa: F[A])(f: A => G[F[B]]): G[F[B]]

so in this specific case, they both go Option[B] => (B => N[Option[C]]) => N[Option[C]]. The only difference is how the Option in N[Option[C]] is handled – IIUC, mapOptionA throws away the None cases, while traverseM flatMaps them, which doesn’t throw them away.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah so do I need to write an instance for Option and use it in the law? I
assumed I would only need the instance for the abstract F.
On Jun 19, 2016 10:17 PM, "Greg Pfeil" notifications@github.com wrote:

In laws/src/main/scala/cats/laws/CollectLaws.scala
#1148 (comment):

  • fa: F[A],
  • f: A => M[Option[B]],
  • g: B => N[Option[C]]
  • )(implicit
  •  M: Applicative[M],
    
  •  N: Applicative[N]
    
  • ): IsEq[Nested[M, N, F[C]]] = {
  • val lhs: Nested[M, N, F[C]] = Nested(fa.mapOptionA(f).map(_.mapOptionA(g)))
  • // TODO is this right and/or meaningful? I can't seem to get the types to
  • // line up for the a straight port of wither (Compose. fmap (wither f). g)
  • // which is how the law is stated for Haskell's Witherable
  • val rhs: Nested[M, N, F[C]] = fa.mapOptionA[Nested[M, N, ?], C](a =>
  •  Nested(f(a).map(_.traverseM(g))))
    
  • lhs <-> rhs
  • }

I think it’s fine that it’s an Option[B] – the type signatures of mapOptionA
and traverseM are both F[A] =...> G[F[B]]

def mapOptionA[G[]: Applicative, A, B](fa: F[A])(f: A =>
G[Option[B]]): G[F[B]]def traverseM[G[
]: Applicative, A, B](fa:
F[A])(f: A => G[F[B]]): G[F[B]]

so in this specific case, they both go Option[B] => (B => N[Option[C]]) =>
N[Option[C]]. The only difference is how the Option in N[Option[C]] is
handled – IIUC, mapOptionA throws away the None cases, while traverseM
flatMaps them, which doesn’t throw them away.


You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/typelevel/cats/pull/1148/files/139659472e3b698dd0e392099f01155f33cbe1b5#r67630044,
or mute the thread
https://github.com/notifications/unsubscribe/AA7sCUSHu09nEV8TRexRAl7JhuoU5XJiks5qNfhMgaJpZM4I5Mbj
.

Copy link
Contributor

Choose a reason for hiding this comment

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

I actually hadn’t thought about it, but yeah, it looks like that’s the case. It looks like Witherable has an instance for Maybe as well as some others.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the help @sellout. I've updated this PR to include what I think is the right representation of the law.

@ceedubs ceedubs changed the title Proof of Concept: Collect (AKA Witherable) type class Proof of Concept: TraverseFilter (AKA Witherable) type class Jun 27, 2016
@ceedubs
Copy link
Contributor Author

ceedubs commented Jun 27, 2016

I renamed the type class from Collect to TraverseFilter, which seems like a slightly better name to me (and relates to MonadFilter). Still open to other name ideas.

ceedubs added a commit to ceedubs/cats that referenced this pull request Jul 15, 2016
Resolves typelevel#1054.

This simply removes `MonadFilter.filterM` to prevent confusion due to
this `filterM` being different than that from Haskell or Scalaz.

This version of `filterM` doesn't seem particularly useful to me. The
only type that comes to mind for which this might be useful is `Option`,
and it's pretty easy to come up with a more straightforward
implementation for the `Option` case. A more general solution for
`filterM` is provided by typelevel#1148, but so far there doesn't seem to be any
interest in that proposal, so I don't want it to hold up resolving
@kailuowang
Copy link
Contributor

Looks great to me. Thanks very much! @ceedubs 👍

* that can essentially have a [[traverse]] and a [[filter]] applied as a single
* combined operation ([[traverseFilter]]).
*
* TODO ceedumbs more info
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add the laws:

A definition of wither must satisfy the following laws:

identity
wither (pure . Just) ≡ pure
composition
Compose . fmap (wither f) . wither g ≡ wither (Compose . fmap (wither f) . g)

Copy link
Contributor

Choose a reason for hiding this comment

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

By which I mean, mention in the comments the laws, I see they are encoded below in the tests.

@johnynek
Copy link
Contributor

This looks really nice.

👍

@@ -55,6 +55,12 @@ import simulacrum.typeclass
val G = Traverse[G]
}

def composeFilter[G[_]: TraverseFilter]: TraverseFilter[λ[α => F[G[α]]]] =
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are people happy with this? Since we only need a Traverse for F but a TraverseFilter for G, and there is a separate compose when G only has Traverse, it doesn't quite fit the common pattern.

@ceedubs
Copy link
Contributor Author

ceedubs commented Jul 16, 2016

@kailuowang @johnynek thanks for the reviews! Since you've shown support for this change, I've fleshed it out a bit. Would you mind taking a second look?

I left a couple minor comments seeking feedback, but I think they are for things that could probably be addressed in a later PR.

The one item that I'm still a little uneasy about is this (from the original PR description):

There's definitely some overlap with MonadFilter here. For example, filter and collect can be defined for both. I'm not sure if there should be a closer relationship between the two. Also syntax enrichment for methods like filter could potentially be ambiguous between the two types (though I don't know how much of a problem this will be in practice as most concrete types that support filter will have it defined on them).

@ceedubs ceedubs changed the title Proof of Concept: TraverseFilter (AKA Witherable) type class TraverseFilter (AKA Witherable) type class Jul 19, 2016
@ceedubs
Copy link
Contributor Author

ceedubs commented Jul 23, 2016

I'm closing this in favor of #1225, where I've cleaned up the git history a bit and added FunctorFilter to resolve the concerns I had raised about overlap between TraverseFilter and MonadFilter.

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.

6 participants