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
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion core/src/main/scala/cats/Composed.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,22 @@ private[cats] trait ComposedFoldable[F[_], G[_]] extends Foldable[λ[α => F[G[
F.foldRight(fga, lb)((ga, lb) => G.foldRight(ga, lb)(f))
}

private[cats] trait ComposedTraverse[F[_], G[_]] extends Traverse[λ[α => F[G[α]]]] with ComposedFoldable[F, G] with ComposedFunctor[F, G] { outer =>
private[cats] trait ComposedTraverse[F[_], G[_]] extends Traverse[λ[α => F[G[α]]]] with ComposedFoldable[F, G] with ComposedFunctor[F, G] {
def F: Traverse[F]
def G: Traverse[G]

override def traverse[H[_]: Applicative, A, B](fga: F[G[A]])(f: A => H[B]): H[F[G[B]]] =
F.traverse(fga)(ga => G.traverse(ga)(f))
}

private[cats] trait ComposedTraverseFilter[F[_], G[_]] extends TraverseFilter[λ[α => F[G[α]]]] with ComposedTraverse[F, G] {
def F: Traverse[F]
def G: TraverseFilter[G]

override def traverseFilter[H[_]: Applicative, A, B](fga: F[G[A]])(f: A => H[Option[B]]): H[F[G[B]]] =
F.traverse[H, G[A], G[B]](fga)(ga => G.traverseFilter(ga)(f))
}

private[cats] trait ComposedReducible[F[_], G[_]] extends Reducible[λ[α => F[G[α]]]] with ComposedFoldable[F, G] { outer =>
def F: Reducible[F]
def G: Reducible[G]
Expand Down
3 changes: 0 additions & 3 deletions core/src/main/scala/cats/MonadFilter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,4 @@ import simulacrum.typeclass

def filter[A](fa: F[A])(f: A => Boolean): F[A] =
flatMap(fa)(a => if (f(a)) pure(a) else empty[A])

def filterM[A](fa: F[A])(f: A => F[Boolean]): F[A] =
flatMap(fa)(a => flatMap(f(a))(b => if (b) pure(a) else empty[A]))
}
6 changes: 6 additions & 0 deletions core/src/main/scala/cats/Traverse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.

new ComposedTraverseFilter[F, G] {
val F = self
val G = TraverseFilter[G]
}

override def map[A, B](fa: F[A])(f: A => B): F[B] =
traverse[Id, A, B](fa)(f)
}
116 changes: 116 additions & 0 deletions core/src/main/scala/cats/TraverseFilter.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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]]).
*
* Must obey the laws defined in cats.laws.TraverseFilterLaws.
*
* 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 Traverse[F] { self =>

/**
* 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[_]: Applicative, A, B](fa: F[A])(f: A => G[Option[B]]): G[F[B]]

/**
* 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] =
traverseFilter[Id, A, B](fa)(f)

/**
* 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> TraverseFilter[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.
*
* 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)

/**
*
* 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))

/**
* 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] =
filterA[Id, A](fa)(f)

override def traverse[G[_], A, B](fa: F[A])(f: A => G[B])(implicit G: Applicative[G]): G[F[B]] =
traverseFilter(fa)(a => G.map(f(a))(Some(_)))
}
12 changes: 9 additions & 3 deletions core/src/main/scala/cats/data/Const.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ final case class Const[A, B](getConst: A) {
def combine(that: Const[A, B])(implicit A: Semigroup[A]): Const[A, B] =
Const(A.combine(getConst, that.getConst))

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

def traverse[F[_], C](f: B => F[C])(implicit F: Applicative[F]): F[Const[A, C]] =
F.pure(retag[C])

Expand Down Expand Up @@ -53,13 +56,16 @@ private[data] sealed abstract class ConstInstances extends ConstInstances0 {
fa.retag[B]
}

implicit def catsDataTraverseForConst[C]: Traverse[Const[C, ?]] = new Traverse[Const[C, ?]] {
def traverse[G[_]: Applicative, A, B](fa: Const[C, A])(f: A => G[B]): G[Const[C, B]] =
fa.traverse(f)
implicit def catsDataTraverseForConst[C]: TraverseFilter[Const[C, ?]] = new TraverseFilter[Const[C, ?]] {
def traverseFilter[G[_]: Applicative, A, B](fa: Const[C, A])(f: A => G[Option[B]]): G[Const[C, B]] =
fa.traverseFilter(f)

def foldLeft[A, B](fa: Const[C, A], b: B)(f: (B, A) => B): B = b

def foldRight[A, B](fa: Const[C, A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = lb

override def traverse[G[_]: Applicative, A, B](fa: Const[C, A])(f: A => G[B]): G[Const[C, B]] =
fa.traverse(f)
}

implicit def catsDataMonoidForConst[A: Monoid, B]: Monoid[Const[A, B]] = new Monoid[Const[A, B]]{
Expand Down
28 changes: 27 additions & 1 deletion core/src/main/scala/cats/data/Nested.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,36 @@ final case class Nested[F[_], G[_], A](value: F[G[A]])

object Nested extends NestedInstances

private[data] sealed abstract class NestedInstances extends NestedInstances1 {
private[data] sealed abstract class NestedInstances extends NestedInstances0 {
implicit def catsDataEqForNested[F[_], G[_], A](implicit FGA: Eq[F[G[A]]]): Eq[Nested[F, G, A]] =
FGA.on(_.value)

implicit def catsDataTraverseFilterForNested[F[_]: Traverse, G[_]: TraverseFilter]: TraverseFilter[Nested[F, G, ?]] =
new TraverseFilter[Nested[F, G, ?]] {
val instance = Traverse[F].composeFilter[G]

def traverseFilter[H[_]: Applicative, A, B](fga: Nested[F, G, A])(f: A => H[Option[B]]): H[Nested[F, G, B]] =
Applicative[H].map(instance.traverseFilter(fga.value)(f))(Nested(_))

def foldLeft[A, B](fga: Nested[F, G, A], b: B)(f: (B, A) => B): B =
instance.foldLeft(fga.value, b)(f)

def foldRight[A, B](fga: Nested[F, G, A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
instance.foldRight(fga.value, lb)(f)

override def traverse[H[_]: Applicative, A, B](fga: Nested[F, G, A])(f: A => H[B]): H[Nested[F, G, B]] =
Applicative[H].map(instance.traverse(fga.value)(f))(Nested(_))

override def map[A, B](fga: Nested[F, G, A])(f: A => B): Nested[F, G, B] =
Nested(instance.map(fga.value)(f))

override def imap[A, B](fga: Nested[F, G, A])(f: A => B)(g: B => A): Nested[F, G, B] =
Nested(instance.imap(fga.value)(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.

@adelbertc this duplicates a lot of what catsDataTraverseForNested has below. It is starting to feel like we should be separating out trait NestedTraverse etc traits like has been done for Composed. What are your thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah a lot of the duplication I couldn't think of a way around because I also wanted to make sure default implementations were overriden where possible, so I had to do a lot of manual overriding/delegation like you do here. If you have ideas on making it cleaner I'm all for it :-)


private[data] sealed abstract class NestedInstances0 extends NestedInstances1 {

implicit def catsDataTraverseForNested[F[_]: Traverse, G[_]: Traverse]: Traverse[Nested[F, G, ?]] =
new Traverse[Nested[F, G, ?]] {
val instance = Traverse[F].compose[G]
Expand Down
12 changes: 9 additions & 3 deletions core/src/main/scala/cats/data/OptionT.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ final case class OptionT[F[_], A](value: F[Option[A]]) {
def ===(that: OptionT[F, A])(implicit eq: Eq[F[Option[A]]]): Boolean =
eq.eqv(value, that.value)

def traverseFilter[G[_], B](f: A => G[Option[B]])(implicit F: Traverse[F], G: Applicative[G]): G[OptionT[F, B]] =
G.map(F.composeFilter(optionInstance).traverseFilter(value)(f))(OptionT.apply)

def traverse[G[_], B](f: A => G[B])(implicit F: Traverse[F], G: Applicative[G]): G[OptionT[F, B]] =
G.map(F.compose(optionInstance).traverse(value)(f))(OptionT.apply)

Expand Down Expand Up @@ -196,8 +199,8 @@ private[data] sealed trait OptionTInstances1 extends OptionTInstances2 {
}

private[data] sealed trait OptionTInstances2 extends OptionTInstances3 {
implicit def catsDataTraverseForOptionT[F[_]](implicit F0: Traverse[F]): Traverse[OptionT[F, ?]] =
new OptionTTraverse[F] { implicit val F = F0 }
implicit def catsDataTraverseForOptionT[F[_]](implicit F0: Traverse[F]): TraverseFilter[OptionT[F, ?]] =
new OptionTTraverseFilter[F] { implicit val F = F0 }
}

private[data] sealed trait OptionTInstances3 {
Expand Down Expand Up @@ -250,9 +253,12 @@ private[data] trait OptionTFoldable[F[_]] extends Foldable[OptionT[F, ?]] {
fa.foldRight(lb)(f)
}

private[data] sealed trait OptionTTraverse[F[_]] extends Traverse[OptionT[F, ?]] with OptionTFoldable[F] {
private[data] sealed trait OptionTTraverseFilter[F[_]] extends TraverseFilter[OptionT[F, ?]] with OptionTFoldable[F] {
implicit def F: Traverse[F]

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

override def traverse[G[_]: Applicative, A, B](fa: OptionT[F, A])(f: A => G[B]): G[OptionT[F, B]] =
fa traverse f
}
Expand Down
13 changes: 10 additions & 3 deletions core/src/main/scala/cats/instances/list.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import cats.data.Xor

trait ListInstances extends cats.kernel.instances.ListInstances {

implicit val catsStdInstancesForList: Traverse[List] with MonadCombine[List] with MonadRec[List] with CoflatMap[List] =
new Traverse[List] with MonadCombine[List] with MonadRec[List] with CoflatMap[List] {
implicit val catsStdInstancesForList: TraverseFilter[List] with MonadCombine[List] with MonadRec[List] with CoflatMap[List] =
new TraverseFilter[List] with MonadCombine[List] with MonadRec[List] with CoflatMap[List] {

def empty[A]: List[A] = Nil

Expand Down Expand Up @@ -63,7 +63,12 @@ trait ListInstances extends cats.kernel.instances.ListInstances {
Eval.defer(loop(fa))
}

def traverse[G[_], A, B](fa: List[A])(f: A => G[B])(implicit G: Applicative[G]): G[List[B]] =
def traverseFilter[G[_], A, B](fa: List[A])(f: A => G[Option[B]])(implicit G: Applicative[G]): G[List[B]] =
foldRight[A, G[List[B]]](fa, Always(G.pure(List.empty))){ (a, lglb) =>
G.map2Eval(f(a), lglb)((ob, l) => ob.fold(l)(_ :: l))
}.value

override def traverse[G[_], A, B](fa: List[A])(f: A => G[B])(implicit G: Applicative[G]): G[List[B]] =
foldRight[A, G[List[B]]](fa, Always(G.pure(List.empty))){ (a, lglb) =>
G.map2Eval(f(a), lglb)(_ :: _)
}.value
Expand All @@ -75,6 +80,8 @@ trait ListInstances extends cats.kernel.instances.ListInstances {
fa.forall(p)

override def isEmpty[A](fa: List[A]): Boolean = fa.isEmpty

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

implicit def catsStdShowForList[A:Show]: Show[List[A]] =
Expand Down
21 changes: 15 additions & 6 deletions core/src/main/scala/cats/instances/option.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import cats.data.Xor

trait OptionInstances extends cats.kernel.instances.OptionInstances {

implicit val catsStdInstancesForOption: Traverse[Option] with MonadError[Option, Unit] with MonadCombine[Option] with MonadRec[Option] with CoflatMap[Option] with Alternative[Option] =
new Traverse[Option] with MonadError[Option, Unit] with MonadCombine[Option] with MonadRec[Option] with CoflatMap[Option] with Alternative[Option] {
implicit val catsStdInstancesForOption: TraverseFilter[Option] with MonadError[Option, Unit] with MonadCombine[Option] with MonadRec[Option] with CoflatMap[Option] with Alternative[Option] =
new TraverseFilter[Option] with MonadError[Option, Unit] with MonadCombine[Option] with MonadRec[Option] with CoflatMap[Option] with Alternative[Option] {

def empty[A]: Option[A] = None

Expand Down Expand Up @@ -53,15 +53,24 @@ trait OptionInstances extends cats.kernel.instances.OptionInstances {
case Some(a) => f(a, lb)
}

def traverse[G[_]: Applicative, A, B](fa: Option[A])(f: A => G[B]): G[Option[B]] =
def raiseError[A](e: Unit): Option[A] = None

def handleErrorWith[A](fa: Option[A])(f: (Unit) => Option[A]): Option[A] = fa orElse f(())

def traverseFilter[G[_], A, B](fa: Option[A])(f: A => G[Option[B]])(implicit G: Applicative[G]): G[Option[B]] =
fa match {
case None => G.pure(None)
case Some(a) => f(a)
}

override def traverse[G[_]: Applicative, A, B](fa: Option[A])(f: A => G[B]): G[Option[B]] =
fa match {
case None => Applicative[G].pure(None)
case Some(a) => Applicative[G].map(f(a))(Some(_))
}

def raiseError[A](e: Unit): Option[A] = None

def handleErrorWith[A](fa: Option[A])(f: (Unit) => Option[A]): Option[A] = fa orElse f(())
override def filter[A](fa: Option[A])(p: A => Boolean): Option[A] =
fa.filter(p)

override def exists[A](fa: Option[A])(p: A => Boolean): Boolean =
fa.exists(p)
Expand Down
21 changes: 16 additions & 5 deletions core/src/main/scala/cats/instances/stream.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ package instances
import cats.syntax.show._

trait StreamInstances extends cats.kernel.instances.StreamInstances {
implicit val catsStdInstancesForStream: Traverse[Stream] with MonadCombine[Stream] with CoflatMap[Stream] =
new Traverse[Stream] with MonadCombine[Stream] with CoflatMap[Stream] {
implicit val catsStdInstancesForStream: TraverseFilter[Stream] with MonadCombine[Stream] with CoflatMap[Stream] =
new TraverseFilter[Stream] with MonadCombine[Stream] with CoflatMap[Stream] {

def empty[A]: Stream[A] = Stream.Empty

Expand Down Expand Up @@ -35,13 +35,20 @@ trait StreamInstances extends cats.kernel.instances.StreamInstances {
if (s.isEmpty) lb else f(s.head, Eval.defer(foldRight(s.tail, lb)(f)))
}

def traverse[G[_], A, B](fa: Stream[A])(f: A => G[B])(implicit G: Applicative[G]): G[Stream[B]] = {
def init: G[Stream[B]] = G.pure(Stream.empty[B])
def traverseFilter[G[_], A, B](fa: Stream[A])(f: A => G[Option[B]])(implicit G: Applicative[G]): G[Stream[B]] = {
// We use foldRight to avoid possible stack overflows. Since
// we don't want to return a Eval[_] instance, we call .value
// at the end.
foldRight(fa, Always(G.pure(Stream.empty[B]))){ (a, lgsb) =>
G.map2Eval(f(a), lgsb)((ob, s) => ob.fold(s)(_ #:: s))
}.value
}

override def traverse[G[_], A, B](fa: Stream[A])(f: A => G[B])(implicit G: Applicative[G]): G[Stream[B]] = {
// We use foldRight to avoid possible stack overflows. Since
// we don't want to return a Eval[_] instance, we call .value
// at the end.
foldRight(fa, Later(init)) { (a, lgsb) =>
foldRight(fa, Always(G.pure(Stream.empty[B]))){ (a, lgsb) =>
G.map2Eval(f(a), lgsb)(_ #:: _)
}.value
}
Expand All @@ -53,6 +60,10 @@ trait StreamInstances extends cats.kernel.instances.StreamInstances {
fa.forall(p)

override def isEmpty[A](fa: Stream[A]): Boolean = fa.isEmpty

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

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

implicit def catsStdShowForStream[A: Show]: Show[Stream[A]] =
Expand Down
Loading