-
-
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
Changes from 7 commits
9c9d9de
f97ba06
4810a9f
d77ab2a
54903f6
218c4c9
344810e
b9e337a
a3f0d26
d323da2
8255d59
bf07dd3
716d701
46f2307
b4c5080
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package cats | ||
|
||
import simulacrum.typeclass | ||
|
||
/** | ||
* `FunctorEmpty[F]` allows you to `map` and filter out elements simultaneously. | ||
*/ | ||
@typeclass | ||
trait FunctorEmpty[F[_]] extends Serializable { | ||
def functor: Functor[F] | ||
|
||
def mapFilter[A, B](fa: F[A])(f: A => Option[B]): F[B] | ||
|
||
def collect[A, B](fa: F[A])(f: PartialFunction[A, B]): F[B] = | ||
mapFilter(fa)(f.lift) | ||
|
||
def flattenOption[A](fa: F[Option[A]]): F[A] = | ||
mapFilter(fa)(identity) | ||
|
||
def filter[A](fa: F[A])(f: A => Boolean): F[A] = | ||
mapFilter(fa)(a => if (f(a)) Some(a) else None) | ||
} | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. This function here allows us to call things like There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Alternatively could we makes functor provide this as a lower priority? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure if it's needed either. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, sorry I missed that 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 commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good to me :) |
||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package cats | ||
|
||
import simulacrum.typeclass | ||
|
||
/** | ||
* `TraverseEmpty`, also known as `Witherable`, represents list-like structures | ||
* that can essentially have a `traverse` and a `filter` applied as a single | ||
* combined operation (`traverseFilter`). | ||
* | ||
* Based on Haskell's [[https://hackage.haskell.org/package/witherable-0.1.3.3/docs/Data-Witherable.html Data.Witherable]] | ||
*/ | ||
|
||
@typeclass | ||
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 commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nope, I agree fully, I'll change it :) |
||
|
||
def traverseFilter[G[_], A, B](fa: F[A])(f: A => G[Option[B]])(implicit G: Applicative[G]): G[F[B]] | ||
|
||
def filterA[G[_], A](fa: F[A])(f: A => G[Boolean])(implicit G: Applicative[G]): G[F[A]] = | ||
traverseFilter(fa)(a => G.map(f(a))(if (_) Some(a) else None)) | ||
|
||
override def mapFilter[A, B](fa: F[A])(f: A => Option[B]): F[B] = | ||
traverseFilter[Id, A, B](fa)(f) | ||
} | ||
|
||
object TraverseEmpty { | ||
def catsTraverseForTraverseEmpty[F[_]](te: TraverseEmpty[F]): Traverse[F] = | ||
te.traverse | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ package data | |
|
||
import cats.Bifunctor | ||
import cats.instances.either._ | ||
import cats.instances.option._ | ||
import cats.syntax.either._ | ||
|
||
/** | ||
|
@@ -505,6 +506,28 @@ private[data] abstract class EitherTInstances extends EitherTInstances1 { | |
def defer[A](fa: => EitherT[F, L, A]): EitherT[F, L, A] = | ||
EitherT(F.defer(fa.value)) | ||
} | ||
|
||
implicit def catsDataTraverseEmptyForEitherT[F[_], L](implicit F0: TraverseEmpty[F]): TraverseEmpty[EitherT[F, L, ?]] = | ||
new EitherTFunctorEmpty[F, L] with TraverseEmpty[EitherT[F, L, ?]] { | ||
implicit def F: FunctorEmpty[F] = F0 | ||
def traverse: Traverse[EitherT[F, L, ?]] = catsDataTraverseForEitherT[F, L](F0.traverse) | ||
|
||
def traverseFilter[G[_], A, B] | ||
(fa: EitherT[F, L, A]) | ||
(f: A => G[Option[B]]) | ||
(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 commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
case Right(a) => G.map(f(a))(_.map(Either.right)) | ||
})(EitherT(_)) | ||
|
||
override def filterA[G[_], A] | ||
(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 commentThe reason will be displayed to describe this comment to others. Learn more. Here and in the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Yeah agree, can always add it back later 👍 |
||
} | ||
} | ||
|
||
private[data] abstract class EitherTInstances1 extends EitherTInstances2 { | ||
|
@@ -536,6 +559,9 @@ private[data] abstract class EitherTInstances1 extends EitherTInstances2 { | |
override def ensureOr[A](fa: EitherT[F, L, A])(error: (A) => L)(predicate: (A) => Boolean): EitherT[F, L, A] = | ||
fa.ensureOr(error)(predicate)(F) | ||
} | ||
|
||
implicit def catsDataFunctorEmptyForEitherT[F[_], L](implicit F0: FunctorEmpty[F]): FunctorEmpty[EitherT[F, L, ?]] = | ||
new EitherTFunctorEmpty[F, L] { implicit def F = F0 } | ||
} | ||
|
||
private[data] abstract class EitherTInstances2 extends EitherTInstances3 { | ||
|
@@ -695,3 +721,24 @@ private[data] sealed trait EitherTOrder[F[_], L, A] extends Order[EitherT[F, L, | |
|
||
override def compare(x: EitherT[F, L, A], y: EitherT[F, L, A]): Int = x compare y | ||
} | ||
|
||
private[data] sealed trait EitherTFunctorEmpty[F[_], E] extends FunctorEmpty[EitherT[F, E, ?]] { | ||
implicit def F: FunctorEmpty[F] | ||
|
||
override def functor: Functor[EitherT[F, E, ?]] = EitherT.catsDataFunctorForEitherT[F, E](F.functor) | ||
|
||
def mapFilter[A, B](fa: EitherT[F, E, A])(f: (A) => Option[B]): EitherT[F, E, B] = | ||
EitherT[F, E, B](F.mapFilter(fa.value)(_.traverse(f))) | ||
|
||
override def collect[A, B](fa: EitherT[F, E, A])(f: PartialFunction[A, B]): EitherT[F, E, B] = { | ||
EitherT[F, E, B](F.mapFilter(fa.value)(_.traverse(f.lift))) | ||
} | ||
|
||
override def flattenOption[A](fa: EitherT[F, E, Option[A]]): EitherT[F, E, A] = { | ||
EitherT[F, E, A](F.flattenOption[Either[E, A]](F.map(fa.value)(Traverse[Either[E, ?]].sequence[Option, A]))) | ||
} | ||
|
||
override def filter[A](fa: EitherT[F, E, A])(f: (A) => Boolean): EitherT[F, E, A] = { | ||
EitherT[F, E, A](F.filter(fa.value)(_.forall(f))) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,6 @@ package cats | |
package data | ||
|
||
|
||
|
||
/** Similar to [[cats.data.Tuple2K]], but for nested composition. | ||
* | ||
* For instance, since both `List` and `Option` have a `Functor`, then so does | ||
|
@@ -54,6 +53,14 @@ private[data] sealed abstract class NestedInstances extends NestedInstances0 { | |
def defer[A](fa: => Nested[F, G, A]): Nested[F, G, A] = | ||
Nested(F.defer(fa.value)) | ||
} | ||
|
||
implicit def catsDataTraverseEmptyForNested[F[_], G[_]](implicit F0: Traverse[F], G0: TraverseEmpty[G]): TraverseEmpty[Nested[F, G, ?]] = | ||
new NestedTraverseEmpty[F, G] { | ||
implicit val F: Traverse[F] = F0 | ||
implicit val G: TraverseEmpty[G] = G0 | ||
} | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: extra newline here. |
||
} | ||
|
||
private[data] sealed abstract class NestedInstances0 extends NestedInstances1 { | ||
|
@@ -315,3 +322,35 @@ private[data] trait NestedInvariantSemigroupalApply[F[_], G[_]] extends Invarian | |
def product[A, B](fa: Nested[F, G, A], fb: Nested[F, G, B]): Nested[F, G, (A, B)] = | ||
Nested(FG.product(fa.value, fb.value)) | ||
} | ||
|
||
private[data] abstract class NestedTraverseEmpty[F[_], G[_]] extends TraverseEmpty[Nested[F, G, ?]] { | ||
implicit val F: Traverse[F] | ||
|
||
implicit val G: TraverseEmpty[G] | ||
|
||
def traverse: Traverse[Nested[F, G, ?]] = Nested.catsDataTraverseForNested(F, G.traverse) | ||
|
||
override def mapFilter[A, B](fa: Nested[F, G, A])(f: (A) => Option[B]): Nested[F, G, B] = | ||
Nested[F, G, B](F.map(fa.value)(G.mapFilter(_)(f))) | ||
|
||
override def collect[A, B](fa: Nested[F, G, A])(f: PartialFunction[A, B]): Nested[F, G, B] = | ||
Nested[F, G, B](F.map(fa.value)(G.collect(_)(f))) | ||
|
||
override def flattenOption[A](fa: Nested[F, G, Option[A]]): Nested[F, G, A] = | ||
Nested[F, G, A](F.map(fa.value)(G.flattenOption)) | ||
|
||
override def filter[A](fa: Nested[F, G, A])(f: (A) => Boolean): Nested[F, G, A] = | ||
Nested[F, G, A](F.map(fa.value)(G.filter(_)(f))) | ||
|
||
override def filterA[H[_], A] | ||
(fa: Nested[F, G, A]) | ||
(f: A => H[Boolean]) | ||
(implicit H: Applicative[H]): H[Nested[F, G, A]] = | ||
H.map(F.traverse(fa.value)(G.filterA[H, A](_)(f)))(Nested[F, G, A]) | ||
|
||
def traverseFilter[H[_], A, B] | ||
(fga: Nested[F, G, A]) | ||
(f: A => H[Option[B]]) | ||
(implicit H: Applicative[H]): H[Nested[F, G, B]] = | ||
H.map(F.traverse[H, G[A], G[B]](fga.value)(ga => G.traverseFilter(ga)(f)))(Nested[F, G, B]) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -121,3 +121,30 @@ trait OptionInstances extends cats.kernel.instances.OptionInstances { | |
} | ||
} | ||
} | ||
|
||
trait OptionInstancesBinCompat0 { | ||
implicit val catsStdTraverseEmptyForOption: TraverseEmpty[Option] = new TraverseEmpty[Option] { | ||
val traverse: Traverse[Option] = cats.instances.option.catsStdInstancesForOption | ||
|
||
override def mapFilter[A, B](fa: Option[A])(f: (A) => Option[B]): Option[B] = fa.flatMap(f) | ||
|
||
override def filter[A](fa: Option[A])(f: (A) => Boolean): Option[A] = fa.filter(f) | ||
|
||
override def collect[A, B](fa: Option[A])(f: PartialFunction[A, B]): Option[B] = fa.collect(f) | ||
|
||
override def flattenOption[A](fa: Option[Option[A]]): Option[A] = fa.flatten | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Minor: I think that this could just be |
||
case Some(a) => f(a) | ||
} | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above |
||
case Some(a) => G.map(f(a))(b => if (b) Some(a) else None) | ||
} | ||
|
||
} | ||
} |
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.