Skip to content

Commit

Permalink
Add FunctorFilter and TraverseFilter (#2405)
Browse files Browse the repository at this point in the history
* Add FunctorEmpty and TraverseEmpty

* Add TraverseEmpty instance for Chain

* Make TraverseEmpty[Chain] serializable

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

* Remove conversion to mainline classes

* Add traverseFilter <-> traverse consistency law

* Make Functor override final

* Rename Functor and Traverse Empty to Filter

* Add Nested FunctorFilter instance

* Address feedback
  • Loading branch information
Luka Jacobowitz authored Sep 3, 2018
1 parent 77c9319 commit 2f129c6
Show file tree
Hide file tree
Showing 33 changed files with 722 additions and 20 deletions.
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

0 comments on commit 2f129c6

Please sign in to comment.