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
69 changes: 69 additions & 0 deletions core/src/main/scala/cats/FunctorFilter.scala
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)
}
62 changes: 62 additions & 0 deletions core/src/main/scala/cats/TraverseFilter.scala
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)
}
23 changes: 23 additions & 0 deletions core/src/main/scala/cats/data/Chain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,29 @@ private[data] sealed abstract class ChainInstances extends ChainInstances1 {
}
}

implicit val catsDataTraverseFilterForChain: TraverseFilter[Chain] = new TraverseFilter[Chain] {
def traverse: Traverse[Chain] = Chain.catsDataInstancesForChain

override def filter[A](fa: Chain[A])(f: A => Boolean): Chain[A] = fa.filter(f)

override def collect[A, B](fa: Chain[A])(f: PartialFunction[A, B]): Chain[B] = fa.collect(f)

override def mapFilter[A, B](fa: Chain[A])(f: A => Option[B]): Chain[B] = fa.collect(Function.unlift(f))

override def flattenOption[A](fa: Chain[Option[A]]): Chain[A] = fa.collect { case Some(a) => a }

def traverseFilter[G[_], A, B](fa: Chain[A])(f: A => G[Option[B]])(implicit G: Applicative[G]): G[Chain[B]] =
fa.foldRight(G.pure(Chain.empty[B]))(
(a, gcb) => G.map2(f(a), gcb)((ob, cb) => ob.fold(cb)(_ +: cb))
)

override def filterA[G[_], A](fa: Chain[A])(f: A => G[Boolean])(implicit G: Applicative[G]): G[Chain[A]] =
fa.foldRight(G.pure(Chain.empty[A]))(
(a, gca) =>
G.map2(f(a), gca)((b, chain) => if (b) a +: chain else chain))

}

}

private[data] sealed abstract class ChainInstances1 extends ChainInstances2 {
Expand Down
19 changes: 19 additions & 0 deletions core/src/main/scala/cats/data/Const.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,25 @@ private[data] sealed abstract class ConstInstances extends ConstInstances0 {
fa.traverse(f)
}

implicit def catsDataTraverseFilterForConst[C]: TraverseFilter[Const[C, ?]] = new TraverseFilter[Const[C, ?]] {

override def mapFilter[A, B](fa: Const[C, A])(f: (A) => Option[B]): Const[C, B] = fa.retag

override def collect[A, B](fa: Const[C, A])(f: PartialFunction[A, B]): Const[C, B] = fa.retag

override def flattenOption[A](fa: Const[C, Option[A]]): Const[C, A] = fa.retag

override def filter[A](fa: Const[C, A])(f: (A) => Boolean): Const[C, A] = fa.retag

def traverseFilter[G[_], A, B](fa: Const[C, A])(f: (A) => G[Option[B]])(implicit G: Applicative[G]): G[Const[C, B]] =
G.pure(fa.retag[B])

override def filterA[G[_], A](fa: Const[C, A])(f: (A) => G[Boolean])(implicit G: Applicative[G]): G[Const[C, A]] =
G.pure(fa)

val traverse: Traverse[Const[C, ?]] = Const.catsDataTraverseForConst[C]
}

implicit def catsDataMonoidForConst[A: Monoid, B]: Monoid[Const[A, B]] = new Monoid[Const[A, B]]{
def empty: Const[A, B] =
Const.empty
Expand Down
54 changes: 53 additions & 1 deletion core/src/main/scala/cats/data/Nested.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,13 +53,25 @@ 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
}
}

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 {
Expand Down Expand Up @@ -315,3 +326,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])
}
39 changes: 38 additions & 1 deletion core/src/main/scala/cats/data/OptionT.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package cats
package data

import cats.instances.option.{catsStdInstancesForOption => optionInstance}
import cats.instances.option.{catsStdInstancesForOption => optionInstance, catsStdTraverseFilterForOption}
import cats.syntax.either._

/**
Expand Down Expand Up @@ -234,6 +234,26 @@ 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 catsDateTraverseFilterForOptionT[F[_]](implicit F0: Traverse[F]): TraverseFilter[OptionT[F, ?]] =
new OptionTFunctorFilter[F] with TraverseFilter[OptionT[F, ?]] {
implicit def F: Functor[F] = F0

val traverse: Traverse[OptionT[F, ?]] = OptionT.catsDataTraverseForOptionT[F]

def traverseFilter[G[_], A, B](fa: OptionT[F, A])
(f: A => G[Option[B]])
(implicit G: Applicative[G]): G[OptionT[F, B]] =
G.map(Traverse[F].traverse[G, Option[A], Option[B]](fa.value) {
oa => TraverseFilter[Option].traverseFilter(oa)(f)
})(OptionT[F, B])

override def filterA[G[_], A](fa: OptionT[F, A])
(f: A => G[Boolean])
(implicit G: Applicative[G]): G[OptionT[F, A]] =
G.map(Traverse[F].traverse(fa.value)(TraverseFilter[Option].filterA[G, A](_)(f)))(OptionT[F, A])

}
}

private[data] sealed abstract class OptionTInstances0 extends OptionTInstances1 {
Expand All @@ -251,6 +271,9 @@ private[data] sealed abstract class OptionTInstances0 extends OptionTInstances1

implicit def catsDataPartialOrderForOptionT[F[_], A](implicit F0: PartialOrder[F[Option[A]]]): PartialOrder[OptionT[F, A]] =
new OptionTPartialOrder[F, A] { implicit val F = F0 }

implicit def catsDateFunctorFilterForOptionT[F[_]](implicit F0: Functor[F]): FunctorFilter[OptionT[F, ?]] =
new OptionTFunctorFilter[F] { implicit val F = F0 }
}

private[data] sealed abstract class OptionTInstances1 extends OptionTInstances2 {
Expand Down Expand Up @@ -387,6 +410,20 @@ private[data] sealed trait OptionTPartialOrder[F[_], A] extends PartialOrder[Opt
override def partialCompare(x: OptionT[F, A], y: OptionT[F, A]): Double = x partialCompare y
}

private[data] sealed trait OptionTFunctorFilter[F[_]] extends FunctorFilter[OptionT[F, ?]] {
implicit def F: Functor[F]

def functor: Functor[OptionT[F, ?]] = OptionT.catsDataFunctorForOptionT[F]

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 trait OptionTOrder[F[_], A] extends Order[OptionT[F, A]] with OptionTPartialOrder[F, A]{
override implicit def F: Order[F[Option[A]]]

Expand Down
7 changes: 6 additions & 1 deletion core/src/main/scala/cats/instances/all.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ trait AllInstancesBinCompat0
with Tuple2InstancesBinCompat0

trait AllInstancesBinCompat1
extends MapInstancesBinCompat0
extends OptionInstancesBinCompat0
with ListInstancesBinCompat0
with VectorInstancesBinCompat0
with StreamInstancesBinCompat0
with MapInstancesBinCompat0
with SortedMapInstancesBinCompat0
26 changes: 26 additions & 0 deletions core/src/main/scala/cats/instances/list.scala
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,29 @@ trait ListInstances extends cats.kernel.instances.ListInstances {
fa.iterator.map(_.show).mkString("List(", ", ", ")")
}
}

trait ListInstancesBinCompat0 {
implicit val catsStdTraverseFilterForList: TraverseFilter[List] = new TraverseFilter[List] {
val traverse: Traverse[List] = cats.instances.list.catsStdInstancesForList

override def mapFilter[A, B](fa: List[A])(f: (A) => Option[B]): List[B] = fa.collect(Function.unlift(f))

override def filter[A](fa: List[A])(f: (A) => Boolean): List[A] = fa.filter(f)

override def collect[A, B](fa: List[A])(f: PartialFunction[A, B]): List[B] = fa.collect(f)

override def flattenOption[A](fa: List[Option[A]]): List[A] = fa.flatten

def traverseFilter[G[_], A, B](fa: List[A])(f: (A) => G[Option[B]])(implicit G: Applicative[G]): G[List[B]] =
fa.foldRight(Eval.now(G.pure(List.empty[B])))(
(x, xse) =>
G.map2Eval(f(x), xse)((i, o) => i.fold(o)(_ :: o))
).value

override def filterA[G[_], A](fa: List[A])(f: (A) => G[Boolean])(implicit G: Applicative[G]): G[List[A]] =
fa.foldRight(Eval.now(G.pure(List.empty[A])))(
(x, xse) =>
G.map2Eval(f(x), xse)((b, list) => if (b) x :: list else list)
).value
}
}
Loading