-
-
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 14 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,69 @@ | ||
package cats | ||
|
||
import simulacrum.typeclass | ||
|
||
/** | ||
* `FunctorFilter[F]` allows you to `map` and filter out elements simultaneously. | ||
*/ | ||
@typeclass | ||
trait FunctorFilter[F[_]] extends Serializable { | ||
def functor: Functor[F] | ||
|
||
/** | ||
* A combined `map` and `filter`. Filtering is handled via `Option` | ||
* instead of `Boolean` such that the output type `B` can be different than | ||
* the input type `A`. | ||
* | ||
* Example: | ||
* {{{ | ||
* scala> import cats.implicits._ | ||
* scala> val m: Map[Int, String] = Map(1 -> "one", 3 -> "three") | ||
* scala> val l: List[Int] = List(1, 2, 3, 4) | ||
* scala> def asString(i: Int): Option[String] = m.get(i) | ||
* scala> l.mapFilter(i => m.get(i)) | ||
* res0: List[String] = List(one, three) | ||
* }}} | ||
*/ | ||
def mapFilter[A, B](fa: F[A])(f: A => Option[B]): F[B] | ||
|
||
/** | ||
* Similar to [[mapFilter]] but uses a partial function instead of a function | ||
* that returns an `Option`. | ||
* | ||
* Example: | ||
* {{{ | ||
* scala> import cats.implicits._ | ||
* scala> val l: List[Int] = List(1, 2, 3, 4) | ||
* scala> FunctorFilter[List].collect(l){ | ||
* | case 1 => "one" | ||
* | case 3 => "three" | ||
* | } | ||
* res0: List[String] = List(one, three) | ||
* }}} | ||
*/ | ||
def collect[A, B](fa: F[A])(f: PartialFunction[A, B]): F[B] = | ||
mapFilter(fa)(f.lift) | ||
|
||
/** | ||
* "Flatten" out a structure by collapsing `Option`s. | ||
* Equivalent to using `mapFilter` with `identity`. | ||
* | ||
* Example: | ||
* {{{ | ||
* scala> import cats.implicits._ | ||
* scala> val l: List[Option[Int]] = List(Some(1), None, Some(3), None) | ||
* scala> l.flattenOption | ||
* res0: List[Int] = List(1, 3) | ||
* }}} | ||
*/ | ||
def flattenOption[A](fa: F[Option[A]]): F[A] = | ||
mapFilter(fa)(identity) | ||
|
||
/** | ||
* Apply a filter to a structure such that the output structure contains all | ||
* `A` elements in the input structure that satisfy the predicate `f` but none | ||
* that don't. | ||
*/ | ||
def filter[A](fa: F[A])(f: A => Boolean): F[A] = | ||
mapFilter(fa)(a => if (f(a)) Some(a) else None) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package cats | ||
|
||
import simulacrum.typeclass | ||
|
||
/** | ||
* `TraverseFilter`, 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 TraverseFilter[F[_]] extends FunctorFilter[F] { | ||
def traverse: Traverse[F] | ||
|
||
final override def functor: Functor[F] = traverse | ||
|
||
/** | ||
* A combined [[traverse]] and [[filter]]. Filtering is handled via `Option` | ||
* instead of `Boolean` such that the output type `B` can be different than | ||
* the input type `A`. | ||
* | ||
* Example: | ||
* {{{ | ||
* scala> import cats.implicits._ | ||
* scala> val m: Map[Int, String] = Map(1 -> "one", 3 -> "three") | ||
* scala> val l: List[Int] = List(1, 2, 3, 4) | ||
* scala> def asString(i: Int): Eval[Option[String]] = Now(m.get(i)) | ||
* scala> val result: Eval[List[String]] = l.traverseFilter(asString) | ||
* scala> result.value | ||
* res0: List[String] = List(one, three) | ||
* }}} | ||
*/ | ||
def traverseFilter[G[_], A, B](fa: F[A])(f: A => G[Option[B]])(implicit G: Applicative[G]): G[F[B]] | ||
|
||
/** | ||
* | ||
* Filter values inside a `G` context. | ||
* | ||
* This is a generalized version of Haskell's [[http://hackage.haskell.org/package/base-4.9.0.0/docs/Control-Monad.html#v:filterM filterM]]. | ||
* [[http://stackoverflow.com/questions/28872396/haskells-filterm-with-filterm-x-true-false-1-2-3 This StackOverflow question]] about `filterM` may be helpful in understanding how it behaves. | ||
* | ||
* Example: | ||
* {{{ | ||
* scala> import cats.implicits._ | ||
* scala> val l: List[Int] = List(1, 2, 3, 4) | ||
* scala> def odd(i: Int): Eval[Boolean] = Now(i % 2 == 1) | ||
* scala> val res: Eval[List[Int]] = l.filterA(odd) | ||
* scala> res.value | ||
* res0: List[Int] = List(1, 3) | ||
* | ||
* scala> List(1, 2, 3).filterA(_ => List(true, false)) | ||
* res1: List[List[Int]] = List(List(1, 2, 3), List(1, 2), List(1, 3), List(1), List(2, 3), List(2), List(3), List()) | ||
* }}} | ||
*/ | ||
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) | ||
} |
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._ | ||
|
||
/** | ||
|
@@ -514,6 +515,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 catsDataTraverseFilterForEitherT[F[_], L](implicit F0: TraverseFilter[F]): TraverseFilter[EitherT[F, L, ?]] = | ||
new EitherTFunctorFilter[F, L] with TraverseFilter[EitherT[F, L, ?]] { | ||
implicit def F: FunctorFilter[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])) | ||
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 { | ||
|
@@ -545,6 +568,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 catsDataFunctorFilterForEitherT[F[_], L](implicit F0: FunctorFilter[F]): FunctorFilter[EitherT[F, L, ?]] = | ||
new EitherTFunctorFilter[F, L] { implicit def F = F0 } | ||
} | ||
|
||
private[data] abstract class EitherTInstances2 extends EitherTInstances3 { | ||
|
@@ -704,3 +730,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 EitherTFunctorFilter[F[_], E] extends FunctorFilter[EitherT[F, E, ?]] { | ||
implicit def F: FunctorFilter[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.functor.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,13 +53,27 @@ 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 catsDataTraverseFilterForNested[F[_], G[_]](implicit F0: Traverse[F], G0: TraverseFilter[G]): TraverseFilter[Nested[F, G, ?]] = | ||
new NestedTraverseFilter[F, G] { | ||
implicit val F: Traverse[F] = F0 | ||
implicit val G: TraverseFilter[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 { | ||
implicit def catsDataTraverseForNested[F[_]: Traverse, G[_]: Traverse]: Traverse[Nested[F, G, ?]] = | ||
new NestedTraverse[F, G] { | ||
val FG: Traverse[λ[α => F[G[α]]]] = Traverse[F].compose[G] | ||
} | ||
|
||
implicit def catsDataFunctorFilterForNested[F[_], G[_]](implicit F0: Functor[F], G0: FunctorFilter[G]): FunctorFilter[Nested[F, G, ?]] = | ||
new NestedFunctorFilter[F, G] { | ||
implicit val F: Functor[F] = F0 | ||
implicit val G: FunctorFilter[G] = G0 | ||
} | ||
} | ||
|
||
private[data] sealed abstract class NestedInstances1 extends NestedInstances2 { | ||
|
@@ -315,3 +328,44 @@ 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 NestedFunctorFilter[F[_], G[_]] extends FunctorFilter[Nested[F, G, ?]] { | ||
implicit val F: Functor[F] | ||
|
||
implicit val G: FunctorFilter[G] | ||
|
||
def functor: Functor[Nested[F, G, ?]] = Nested.catsDataFunctorForNested(F, G.functor) | ||
|
||
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))) | ||
} | ||
|
||
private[data] abstract class NestedTraverseFilter[F[_], G[_]] | ||
extends NestedFunctorFilter[F, G] with TraverseFilter[Nested[F, G, ?]] { | ||
implicit val F: Traverse[F] | ||
|
||
implicit val G: TraverseFilter[G] | ||
|
||
def traverse: Traverse[Nested[F, G, ?]] = Nested.catsDataTraverseForNested(F, G.traverse) | ||
|
||
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 |
---|---|---|
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. There was 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. No idea, just took what was there from 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'm having trouble tracking down the history in cats-mtl, but I think that we should have the |
||
new FunctorFilter[OptionT[F, ?]] { | ||
override val functor: Functor[OptionT[F, ?]] = OptionT.catsDataFunctorForOptionT[F] | ||
|
||
override def mapFilter[A, B](fa: OptionT[F, A])(f: (A) => Option[B]): OptionT[F, B] = fa.subflatMap(f) | ||
|
||
override def collect[A, B](fa: OptionT[F, A])(f: PartialFunction[A, B]): OptionT[F, B] = fa.subflatMap(f.lift) | ||
|
||
override def flattenOption[A](fa: OptionT[F, Option[A]]): OptionT[F, A] = fa.subflatMap(identity) | ||
|
||
override def filter[A](fa: OptionT[F, A])(f: (A) => Boolean): OptionT[F, A] = fa.filter(f) | ||
} | ||
} | ||
} | ||
|
||
private[data] sealed abstract class OptionTInstances0 extends OptionTInstances1 { | ||
|
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 anull
. I think that we should just use theSome
constructor instead (and I believe that we have followed this convention elsewhere in Cats).