From 6d6eb796ead13abe3e624e78b88e97875d584a79 Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Sat, 23 Jul 2016 11:42:55 -0400 Subject: [PATCH 1/7] Add FunctorFilter and TraverseFilter `TraverseFilter` is a port of Haskell's Data.Witherable, and represents structures that support doing a combined `traverse` and `filter` as a single operation. `FunctorFilter` is similar but is limited to a combined `map` and `filter` as a single operation. The main reason that I've added `FunctorFilter` is to extract the commonality between `TraverseFilter` and `MonadFilter` so that they don't have collisions in methods such as `filter`. Some benefits of these type classes: - They provide `filterM` (actually a more general version of it - `filterA`), which resolves #1054. - They provide `collect` which is a handy method that is often used on standard library collections but had been absent in Cats. --- core/src/main/scala/cats/Composed.scala | 18 ++++- core/src/main/scala/cats/Functor.scala | 6 ++ core/src/main/scala/cats/FunctorFilter.scala | 62 ++++++++++++++++++ core/src/main/scala/cats/MonadFilter.scala | 8 +-- core/src/main/scala/cats/Traverse.scala | 6 ++ core/src/main/scala/cats/TraverseFilter.scala | 65 +++++++++++++++++++ core/src/main/scala/cats/data/Const.scala | 12 +++- core/src/main/scala/cats/data/Nested.scala | 45 ++++++++++++- core/src/main/scala/cats/data/OptionT.scala | 22 +++++-- core/src/main/scala/cats/instances/list.scala | 13 +++- .../main/scala/cats/instances/option.scala | 21 ++++-- .../main/scala/cats/instances/stream.scala | 21 ++++-- .../main/scala/cats/instances/vector.scala | 15 ++++- core/src/main/scala/cats/syntax/all.scala | 2 + .../scala/cats/syntax/functorFilter.scala | 12 ++++ .../scala/cats/syntax/traverseFilter.scala | 12 ++++ .../scala/cats/laws/FunctorFilterLaws.scala | 35 ++++++++++ .../scala/cats/laws/MonadFilterLaws.scala | 2 +- .../scala/cats/laws/TraverseFilterLaws.scala | 33 ++++++++++ .../laws/discipline/FunctorFilterTests.scala | 31 +++++++++ .../laws/discipline/MonadFilterTests.scala | 18 +++-- .../laws/discipline/TraverseFilterTests.scala | 46 +++++++++++++ .../test/scala/cats/tests/ConstTests.scala | 4 +- .../src/test/scala/cats/tests/ListTests.scala | 6 +- .../test/scala/cats/tests/ListWrapper.scala | 20 +++--- .../test/scala/cats/tests/NestedTests.scala | 21 ++++++ .../test/scala/cats/tests/OptionTTests.scala | 9 +-- .../test/scala/cats/tests/OptionTests.scala | 4 +- .../test/scala/cats/tests/StreamTests.scala | 6 +- .../test/scala/cats/tests/VectorTests.scala | 6 +- 30 files changed, 514 insertions(+), 67 deletions(-) create mode 100644 core/src/main/scala/cats/FunctorFilter.scala create mode 100644 core/src/main/scala/cats/TraverseFilter.scala create mode 100644 core/src/main/scala/cats/syntax/functorFilter.scala create mode 100644 core/src/main/scala/cats/syntax/traverseFilter.scala create mode 100644 laws/src/main/scala/cats/laws/FunctorFilterLaws.scala create mode 100644 laws/src/main/scala/cats/laws/TraverseFilterLaws.scala create mode 100644 laws/src/main/scala/cats/laws/discipline/FunctorFilterTests.scala create mode 100644 laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala diff --git a/core/src/main/scala/cats/Composed.scala b/core/src/main/scala/cats/Composed.scala index 67a790d578..e71e5aef21 100644 --- a/core/src/main/scala/cats/Composed.scala +++ b/core/src/main/scala/cats/Composed.scala @@ -63,7 +63,7 @@ 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] @@ -71,6 +71,22 @@ private[cats] trait ComposedTraverse[F[_], G[_]] extends Traverse[λ[α => F[G[ 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 ComposedFunctorFilter[F[_], G[_]] extends FunctorFilter[λ[α => F[G[α]]]] with ComposedFunctor[F, G] { + def F: Functor[F] + def G: FunctorFilter[G] + + override def mapFilter[A, B](fga: F[G[A]])(f: A => Option[B]): F[G[B]] = + F.map(fga)(G.mapFilter(_)(f)) +} + private[cats] trait ComposedReducible[F[_], G[_]] extends Reducible[λ[α => F[G[α]]]] with ComposedFoldable[F, G] { outer => def F: Reducible[F] def G: Reducible[G] diff --git a/core/src/main/scala/cats/Functor.scala b/core/src/main/scala/cats/Functor.scala index 1da9494a39..d73ac6b606 100644 --- a/core/src/main/scala/cats/Functor.scala +++ b/core/src/main/scala/cats/Functor.scala @@ -58,6 +58,12 @@ import simulacrum.typeclass val G = Functor[G] } + def composeFilter[G[_]: FunctorFilter]: FunctorFilter[λ[α => F[G[α]]]] = + new ComposedFunctorFilter[F, G] { + val F = self + val G = FunctorFilter[G] + } + override def composeContravariant[G[_]: Contravariant]: Contravariant[λ[α => F[G[α]]]] = new ComposedCovariantContravariant[F, G] { val F = self diff --git a/core/src/main/scala/cats/FunctorFilter.scala b/core/src/main/scala/cats/FunctorFilter.scala new file mode 100644 index 0000000000..921dd70b67 --- /dev/null +++ b/core/src/main/scala/cats/FunctorFilter.scala @@ -0,0 +1,62 @@ +package cats + +import simulacrum.typeclass + +@typeclass trait FunctorFilter[F[_]] extends 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> 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) + + /** + * 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) +} diff --git a/core/src/main/scala/cats/MonadFilter.scala b/core/src/main/scala/cats/MonadFilter.scala index 80129ebc82..e2099a49e2 100644 --- a/core/src/main/scala/cats/MonadFilter.scala +++ b/core/src/main/scala/cats/MonadFilter.scala @@ -4,15 +4,15 @@ import simulacrum.typeclass /** * a Monad equipped with an additional method which allows us to - * create an "Empty" value for the Monad (for whatever "empty" makes + * create an "empty" value for the Monad (for whatever "empty" makes * sense for that particular monad). This is of particular interest to * us since it allows us to add a `filter` method to a Monad, which is * used when pattern matching or using guards in for comprehensions. */ -@typeclass trait MonadFilter[F[_]] extends Monad[F] { +@typeclass trait MonadFilter[F[_]] extends Monad[F] with FunctorFilter[F] { def empty[A]: F[A] - def filter[A](fa: F[A])(f: A => Boolean): F[A] = - flatMap(fa)(a => if (f(a)) pure(a) else empty[A]) + override def mapFilter[A, B](fa: F[A])(f: A => Option[B]): F[B] = + flatMap(fa)(a => f(a).fold(empty[B])(pure)) } diff --git a/core/src/main/scala/cats/Traverse.scala b/core/src/main/scala/cats/Traverse.scala index 59aefe37b2..ced79fe559 100644 --- a/core/src/main/scala/cats/Traverse.scala +++ b/core/src/main/scala/cats/Traverse.scala @@ -110,6 +110,12 @@ import simulacrum.typeclass val G = Traverse[G] } + def composeFilter[G[_]: TraverseFilter]: TraverseFilter[λ[α => F[G[α]]]] = + 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) } diff --git a/core/src/main/scala/cats/TraverseFilter.scala b/core/src/main/scala/cats/TraverseFilter.scala new file mode 100644 index 0000000000..9e6c1ebac6 --- /dev/null +++ b/core/src/main/scala/cats/TraverseFilter.scala @@ -0,0 +1,65 @@ +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] with FunctorFilter[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]] + + override def mapFilter[A, B](fa: F[A])(f: A => Option[B]): F[B] = + traverseFilter[Id, A, B](fa)(f) + + /** + * + * 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 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(_))) +} diff --git a/core/src/main/scala/cats/data/Const.scala b/core/src/main/scala/cats/data/Const.scala index 154c696aa7..227cd6e685 100644 --- a/core/src/main/scala/cats/data/Const.scala +++ b/core/src/main/scala/cats/data/Const.scala @@ -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]) @@ -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]]{ diff --git a/core/src/main/scala/cats/data/Nested.scala b/core/src/main/scala/cats/data/Nested.scala index 6675e5642a..9af568baf7 100644 --- a/core/src/main/scala/cats/data/Nested.scala +++ b/core/src/main/scala/cats/data/Nested.scala @@ -27,10 +27,18 @@ 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 NestedTraverseFilter[F, G] { + val FG: TraverseFilter[λ[α => F[G[α]]]] = Traverse[F].composeFilter[G] + } +} + +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] @@ -118,7 +126,14 @@ private[data] sealed abstract class NestedInstances8 extends NestedInstances9 { } } -private[data] sealed abstract class NestedInstances9 { +private[data] sealed abstract class NestedInstances9 extends NestedInstances10 { + implicit def catsDataFunctorFilterForNested[F[_]: Functor, G[_]: FunctorFilter]: FunctorFilter[Nested[F, G, ?]] = + new NestedFunctorFilter[F, G] { + val FG: FunctorFilter[λ[α => F[G[α]]]] = Functor[F].composeFilter[G] + } +} + +private[data] sealed abstract class NestedInstances10 { implicit def catsDataInvariantForNestedContravariant[F[_]: Invariant, G[_]: Contravariant]: Invariant[Nested[F, G, ?]] = new NestedInvariant[F, G] { val FG: Invariant[λ[α => F[G[α]]]] = Invariant[F].composeContravariant[G] @@ -139,6 +154,32 @@ private[data] trait NestedFunctor[F[_], G[_]] extends Functor[Nested[F, G, ?]] w Nested(FG.map(fga.value)(f)) } +private[data] trait NestedFunctorFilter[F[_], G[_]] extends FunctorFilter[Nested[F, G, ?]] with NestedFunctor[F, G] { + override def FG: FunctorFilter[λ[α => F[G[α]]]] + + override def mapFilter[A, B](fga: Nested[F, G, A])(f: A => Option[B]): Nested[F, G, B] = + Nested(FG.mapFilter(fga.value)(f)) + + override def collect[A, B](fga: Nested[F, G, A])(f: PartialFunction[A, B]): Nested[F, G, B] = + Nested(FG.collect(fga.value)(f)) + + override def filter[A](fga: Nested[F, G, A])(f: A => Boolean): Nested[F, G, A] = + Nested(FG.filter(fga.value)(f)) +} + +private[data] trait NestedTraverseFilter[F[_], G[_]] extends TraverseFilter[Nested[F, G, ?]] with NestedFunctorFilter[F, G] with NestedTraverse[F, G] { + override def FG: TraverseFilter[λ[α => F[G[α]]]] + + override def traverseFilter[H[_]: Applicative, A, B](fga: Nested[F, G, A])(f: A => H[Option[B]]): H[Nested[F, G, B]] = + Applicative[H].map(FG.traverseFilter(fga.value)(f))(Nested(_)) + + override def collect[A, B](fga: Nested[F, G, A])(f: PartialFunction[A, B]): Nested[F, G, B] = + Nested(FG.collect(fga.value)(f)) + + override def filter[A](fga: Nested[F, G, A])(f: A => Boolean): Nested[F, G, A] = + Nested(FG.filter(fga.value)(f)) +} + private[data] trait NestedApply[F[_], G[_]] extends Apply[Nested[F, G, ?]] with NestedFunctor[F, G] { override def FG: Apply[λ[α => F[G[α]]]] diff --git a/core/src/main/scala/cats/data/OptionT.scala b/core/src/main/scala/cats/data/OptionT.scala index 8927ec0c7c..2921848a03 100644 --- a/core/src/main/scala/cats/data/OptionT.scala +++ b/core/src/main/scala/cats/data/OptionT.scala @@ -30,6 +30,9 @@ final case class OptionT[F[_], A](value: F[Option[A]]) { def semiflatMap[B](f: A => F[B])(implicit F: Monad[F]): OptionT[F, B] = flatMap(a => OptionT.liftF(f(a))) + def mapFilter[B](f: A => Option[B])(implicit F: Functor[F]): OptionT[F, B] = + OptionT(F.map(value)(_.flatMap(f))) + def flatMap[B](f: A => OptionT[F, B])(implicit F: Monad[F]): OptionT[F, B] = flatMapF(a => f(a).value) @@ -99,6 +102,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) @@ -219,19 +225,22 @@ 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 { - implicit def catsDataFunctorForOptionT[F[_]](implicit F0: Functor[F]): Functor[OptionT[F, ?]] = + implicit def catsDataFunctorForOptionT[F[_]](implicit F0: Functor[F]): FunctorFilter[OptionT[F, ?]] = new OptionTFunctor[F] { implicit val F = F0 } } -private[data] trait OptionTFunctor[F[_]] extends Functor[OptionT[F, ?]] { +private[data] trait OptionTFunctor[F[_]] extends FunctorFilter[OptionT[F, ?]] { implicit def F: Functor[F] override def map[A, B](fa: OptionT[F, A])(f: A => B): OptionT[F, B] = fa.map(f) + + override def mapFilter[A, B](fa: OptionT[F, A])(f: A => Option[B]): OptionT[F, B] = + fa.mapFilter(f) } private[data] trait OptionTMonad[F[_]] extends Monad[OptionT[F, ?]] { @@ -273,9 +282,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 } diff --git a/core/src/main/scala/cats/instances/list.scala b/core/src/main/scala/cats/instances/list.scala index 437c677653..9b38b6af21 100644 --- a/core/src/main/scala/cats/instances/list.scala +++ b/core/src/main/scala/cats/instances/list.scala @@ -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 @@ -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 @@ -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]] = diff --git a/core/src/main/scala/cats/instances/option.scala b/core/src/main/scala/cats/instances/option.scala index c3ba6b635a..7ce5d7ffc9 100644 --- a/core/src/main/scala/cats/instances/option.scala +++ b/core/src/main/scala/cats/instances/option.scala @@ -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 @@ -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) diff --git a/core/src/main/scala/cats/instances/stream.scala b/core/src/main/scala/cats/instances/stream.scala index 9d99563817..f12b285466 100644 --- a/core/src/main/scala/cats/instances/stream.scala +++ b/core/src/main/scala/cats/instances/stream.scala @@ -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 @@ -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 } @@ -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]] = diff --git a/core/src/main/scala/cats/instances/vector.scala b/core/src/main/scala/cats/instances/vector.scala index caec99171a..7e59621010 100644 --- a/core/src/main/scala/cats/instances/vector.scala +++ b/core/src/main/scala/cats/instances/vector.scala @@ -8,8 +8,8 @@ import scala.collection.+: import scala.collection.immutable.VectorBuilder trait VectorInstances extends cats.kernel.instances.VectorInstances { - implicit val catsStdInstancesForVector: Traverse[Vector] with MonadCombine[Vector] with CoflatMap[Vector] = - new Traverse[Vector] with MonadCombine[Vector] with CoflatMap[Vector] { + implicit val catsStdInstancesForVector: TraverseFilter[Vector] with MonadCombine[Vector] with CoflatMap[Vector] = + new TraverseFilter[Vector] with MonadCombine[Vector] with CoflatMap[Vector] { def empty[A]: Vector[A] = Vector.empty[A] @@ -44,9 +44,14 @@ trait VectorInstances extends cats.kernel.instances.VectorInstances { Eval.defer(loop(0)) } + def traverseFilter[G[_], A, B](fa: Vector[A])(f: A => G[Option[B]])(implicit G: Applicative[G]): G[Vector[B]] = + foldRight[A, G[Vector[B]]](fa, Always(G.pure(Vector.empty))){ (a, lgvb) => + G.map2Eval(f(a), lgvb)((ob, v) => ob.fold(v)(_ +: v)) + }.value + override def size[A](fa: Vector[A]): Long = fa.size.toLong - def traverse[G[_], A, B](fa: Vector[A])(f: A => G[B])(implicit G: Applicative[G]): G[Vector[B]] = + override def traverse[G[_], A, B](fa: Vector[A])(f: A => G[B])(implicit G: Applicative[G]): G[Vector[B]] = foldRight[A, G[Vector[B]]](fa, Always(G.pure(Vector.empty))){ (a, lgvb) => G.map2Eval(f(a), lgvb)(_ +: _) }.value @@ -55,6 +60,10 @@ trait VectorInstances extends cats.kernel.instances.VectorInstances { fa.exists(p) override def isEmpty[A](fa: Vector[A]): Boolean = fa.isEmpty + + override def filter[A](fa: Vector[A])(f: A => Boolean): Vector[A] = fa.filter(f) + + override def collect[A, B](fa: Vector[A])(f: PartialFunction[A, B]): Vector[B] = fa.collect(f) } implicit def catsStdShowForVector[A:Show]: Show[Vector[A]] = diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index 6420340748..a7373ca63c 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -19,6 +19,7 @@ trait AllSyntax with FlatMapSyntax with FoldableSyntax with FunctorSyntax + with FunctorFilterSyntax with GroupSyntax with InvariantSyntax with ListSyntax @@ -37,6 +38,7 @@ trait AllSyntax with SplitSyntax with StrongSyntax with TransLiftSyntax + with TraverseFilterSyntax with TraverseSyntax with ValidatedSyntax with WriterSyntax diff --git a/core/src/main/scala/cats/syntax/functorFilter.scala b/core/src/main/scala/cats/syntax/functorFilter.scala new file mode 100644 index 0000000000..eb7757bee5 --- /dev/null +++ b/core/src/main/scala/cats/syntax/functorFilter.scala @@ -0,0 +1,12 @@ +package cats +package syntax + +private[syntax] trait FunctorFilterSyntax1 { + implicit def catsSyntaxUFunctorFilter[FA](fa: FA)(implicit U: Unapply[FunctorFilter, FA]): FunctorFilter.Ops[U.M, U.A] = + new FunctorFilter.Ops[U.M, U.A]{ + val self = U.subst(fa) + val typeClassInstance = U.TC + } +} + +trait FunctorFilterSyntax extends FunctorFilter.ToFunctorFilterOps with FunctorFilterSyntax1 diff --git a/core/src/main/scala/cats/syntax/traverseFilter.scala b/core/src/main/scala/cats/syntax/traverseFilter.scala new file mode 100644 index 0000000000..4317db2929 --- /dev/null +++ b/core/src/main/scala/cats/syntax/traverseFilter.scala @@ -0,0 +1,12 @@ +package cats +package syntax + +trait TraverseFilterSyntax extends TraverseFilter.ToTraverseFilterOps with TraverseFilterSyntax1 + +private[syntax] trait TraverseFilterSyntax1 { + implicit def catsSyntaxUTraverseFilter[FA](fa: FA)(implicit U: Unapply[TraverseFilter, FA]): TraverseFilter.Ops[U.M, U.A] = + new TraverseFilter.Ops[U.M, U.A]{ + val self = U.subst(fa) + val typeClassInstance = U.TC + } +} diff --git a/laws/src/main/scala/cats/laws/FunctorFilterLaws.scala b/laws/src/main/scala/cats/laws/FunctorFilterLaws.scala new file mode 100644 index 0000000000..d79de5dadc --- /dev/null +++ b/laws/src/main/scala/cats/laws/FunctorFilterLaws.scala @@ -0,0 +1,35 @@ +package cats +package laws + +import cats.implicits._ + +trait FunctorFilterLaws[F[_]] extends FunctorLaws[F] { + implicit override def F: FunctorFilter[F] + + def mapFilterComposition[A, B, C]( + fa: F[A], + f: A => Option[B], + g: B => Option[C] + ): IsEq[F[C]] = { + + val lhs: F[C] = fa.mapFilter(f).mapFilter(g) + val rhs: F[C] = fa.mapFilter(a => f(a).flatMap(g)) + lhs <-> rhs + } + + /** + * Combined with the functor identity law, this implies a `mapFilter` identity + * law (when `f` is the identity function). + */ + def mapFilterMapConsistency[A, B]( + fa: F[A], + f: A => B + ): IsEq[F[B]] = { + fa.mapFilter(f andThen (_.some)) <-> fa.map(f) + } +} + +object FunctorFilterLaws { + def apply[F[_]](implicit ev: FunctorFilter[F]): FunctorFilterLaws[F] = + new FunctorFilterLaws[F] { def F: FunctorFilter[F] = ev } +} diff --git a/laws/src/main/scala/cats/laws/MonadFilterLaws.scala b/laws/src/main/scala/cats/laws/MonadFilterLaws.scala index a92f9273e1..9116df2bba 100644 --- a/laws/src/main/scala/cats/laws/MonadFilterLaws.scala +++ b/laws/src/main/scala/cats/laws/MonadFilterLaws.scala @@ -6,7 +6,7 @@ import cats.syntax.all._ /** * Laws that must be obeyed by any `MonadFilter`. */ -trait MonadFilterLaws[F[_]] extends MonadLaws[F] { +trait MonadFilterLaws[F[_]] extends MonadLaws[F] with FunctorFilterLaws[F] { implicit override def F: MonadFilter[F] def monadFilterLeftEmpty[A, B](f: A => F[B]): IsEq[F[B]] = diff --git a/laws/src/main/scala/cats/laws/TraverseFilterLaws.scala b/laws/src/main/scala/cats/laws/TraverseFilterLaws.scala new file mode 100644 index 0000000000..8620d9b926 --- /dev/null +++ b/laws/src/main/scala/cats/laws/TraverseFilterLaws.scala @@ -0,0 +1,33 @@ +package cats +package laws + +import cats.data.Nested +import cats.implicits._ + +trait TraverseFilterLaws[F[_]] extends TraverseLaws[F] with FunctorFilterLaws[F] { + implicit override def F: TraverseFilter[F] + + def traverseFilterIdentity[G[_]:Applicative, A](fa: F[A]): IsEq[G[F[A]]] = { + fa.traverseFilter(_.some.pure[G]) <-> fa.pure[G] + } + + def traverseFilterComposition[A, B, C, M[_], N[_]]( + fa: F[A], + f: A => M[Option[B]], + g: B => N[Option[C]] + )(implicit + M: Applicative[M], + N: Applicative[N] + ): IsEq[Nested[M, N, F[C]]] = { + + val lhs: Nested[M, N, F[C]] = Nested(fa.traverseFilter(f).map(_.traverseFilter(g))) + val rhs: Nested[M, N, F[C]] = fa.traverseFilter[Nested[M, N, ?], C](a => + Nested(f(a).map(_.traverseFilter(g)))) + lhs <-> rhs + } +} + +object TraverseFilterLaws { + def apply[F[_]](implicit ev: TraverseFilter[F]): TraverseFilterLaws[F] = + new TraverseFilterLaws[F] { def F: TraverseFilter[F] = ev } +} diff --git a/laws/src/main/scala/cats/laws/discipline/FunctorFilterTests.scala b/laws/src/main/scala/cats/laws/discipline/FunctorFilterTests.scala new file mode 100644 index 0000000000..86ab17723e --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/FunctorFilterTests.scala @@ -0,0 +1,31 @@ +package cats +package laws +package discipline + +import org.scalacheck.Arbitrary +import org.scalacheck.Prop +import Prop._ + +trait FunctorFilterTests[F[_]] extends FunctorTests[F] { + def laws: FunctorFilterLaws[F] + + def functorFilter[A: Arbitrary, B: Arbitrary, C: Arbitrary](implicit + ArbFA: Arbitrary[F[A]], + ArbAOB: Arbitrary[A => Option[B]], + ArbBOC: Arbitrary[B => Option[C]], + ArbAB: Arbitrary[A => C], + EqFA: Eq[F[A]], + EqFC: Eq[F[C]] + ): RuleSet = { + new DefaultRuleSet( + name = "functorFilter", + parent = Some(functor[A, B, C]), + "mapFilter composition" -> forAll(laws.mapFilterComposition[A, B, C] _), + "mapFilter map consistency" -> forAll(laws.mapFilterMapConsistency[A, C] _)) + } +} + +object FunctorFilterTests { + def apply[F[_]: FunctorFilter]: FunctorFilterTests[F] = + new FunctorFilterTests[F] { def laws: FunctorFilterLaws[F] = FunctorFilterLaws[F] } +} diff --git a/laws/src/main/scala/cats/laws/discipline/MonadFilterTests.scala b/laws/src/main/scala/cats/laws/discipline/MonadFilterTests.scala index 4d6bdf177b..bfdb555fd6 100644 --- a/laws/src/main/scala/cats/laws/discipline/MonadFilterTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/MonadFilterTests.scala @@ -7,7 +7,7 @@ import org.scalacheck.Arbitrary import org.scalacheck.Prop import Prop._ -trait MonadFilterTests[F[_]] extends MonadTests[F] { +trait MonadFilterTests[F[_]] extends MonadTests[F] with FunctorFilterTests[F] { def laws: MonadFilterLaws[F] def monadFilter[A: Arbitrary: Eq, B: Arbitrary: Eq, C: Arbitrary: Eq](implicit @@ -22,12 +22,16 @@ trait MonadFilterTests[F[_]] extends MonadTests[F] { EqFABC: Eq[F[(A, B, C)]], iso: Isomorphisms[F] ): RuleSet = { - new DefaultRuleSet( - name = "monadFilter", - parent = Some(monad[A, B, C]), - "monadFilter left empty" -> forAll(laws.monadFilterLeftEmpty[A, B] _), - "monadFilter right empty" -> forAll(laws.monadFilterRightEmpty[A, B] _), - "monadFilter consistency" -> forAll(laws.monadFilterConsistency[A, B] _)) + new RuleSet { + def name: String = "monadFilter" + def bases: Seq[(String, RuleSet)] = Nil + def parents: Seq[RuleSet] = Seq(monad[A, B, C], functorFilter[A, B, C]) + def props: Seq[(String, Prop)] = Seq( + "monadFilter left empty" -> forAll(laws.monadFilterLeftEmpty[A, B] _), + "monadFilter right empty" -> forAll(laws.monadFilterRightEmpty[A, B] _), + "monadFilter consistency" -> forAll(laws.monadFilterConsistency[A, B] _) + ) + } } } diff --git a/laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala b/laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala new file mode 100644 index 0000000000..a7a0ee5b38 --- /dev/null +++ b/laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala @@ -0,0 +1,46 @@ +package cats +package laws +package discipline + +import org.scalacheck.{Prop, Arbitrary} +import Prop._ + +trait TraverseFilterTests[F[_]] extends TraverseTests[F] with FunctorFilterTests[F] { + def laws: TraverseFilterLaws[F] + + def traverseFilter[A: Arbitrary, B: Arbitrary, C: Arbitrary, M: Arbitrary, X[_]: Applicative, Y[_]: Applicative](implicit + ArbFA: Arbitrary[F[A]], + ArbXB: Arbitrary[X[B]], + ArbYB: Arbitrary[Y[B]], + ArbYC: Arbitrary[Y[C]], + ArbAXOB: Arbitrary[A => X[Option[B]]], + ArbBYOC: Arbitrary[B => Y[Option[C]]], + M: Monoid[M], + EqFA: Eq[F[A]], + EqFC: Eq[F[C]], + EqM: Eq[M], + EqXYFC: Eq[X[Y[F[C]]]], + EqXFA: Eq[X[F[A]]], + EqXFB: Eq[X[F[B]]], + EqYFB: Eq[Y[F[B]]] + ): RuleSet = { + implicit def EqXFBYFB : Eq[(X[F[B]], Y[F[B]])] = new Eq[(X[F[B]], Y[F[B]])] { + override def eqv(x: (X[F[B]], Y[F[B]]), y: (X[F[B]], Y[F[B]])): Boolean = + EqXFB.eqv(x._1, y._1) && EqYFB.eqv(x._2, y._2) + } + new RuleSet { + def name: String = "collect" + def bases: Seq[(String, RuleSet)] = Nil + def parents: Seq[RuleSet] = Seq(traverse[A, B, C, M, X, Y], functorFilter[A, B, C]) + def props: Seq[(String, Prop)] = Seq( + "traverseFilter identity" -> forAll(laws.traverseFilterIdentity[X, A] _), + "traverseFilter composition" -> forAll(laws.traverseFilterComposition[A, B, C, X, Y] _) + ) + } + } +} + +object TraverseFilterTests { + def apply[F[_]: TraverseFilter]: TraverseFilterTests[F] = + new TraverseFilterTests[F] { def laws: TraverseFilterLaws[F] = TraverseFilterLaws[F] } +} diff --git a/tests/src/test/scala/cats/tests/ConstTests.scala b/tests/src/test/scala/cats/tests/ConstTests.scala index ae0f0c0017..e485b5482a 100644 --- a/tests/src/test/scala/cats/tests/ConstTests.scala +++ b/tests/src/test/scala/cats/tests/ConstTests.scala @@ -18,8 +18,8 @@ class ConstTests extends CatsSuite { checkAll("Const[String, Int]", ApplicativeTests[Const[String, ?]].applicative[Int, Int, Int]) checkAll("Applicative[Const[String, ?]]", SerializableTests.serializable(Applicative[Const[String, ?]])) - checkAll("Const[String, Int] with Option", TraverseTests[Const[String, ?]].traverse[Int, Int, Int, Int, Option, Option]) - checkAll("Traverse[Const[String, ?]]", SerializableTests.serializable(Traverse[Const[String, ?]])) + checkAll("Const[String, Int] with Option", TraverseFilterTests[Const[String, ?]].traverseFilter[Int, Int, Int, Int, Option, Option]) + checkAll("TraverseFilter[Const[String, ?]]", SerializableTests.serializable(TraverseFilter[Const[String, ?]])) // Get Apply[Const[C : Semigroup, ?]], not Applicative[Const[C : Monoid, ?]] { diff --git a/tests/src/test/scala/cats/tests/ListTests.scala b/tests/src/test/scala/cats/tests/ListTests.scala index fc92265ce8..b7c9ece99e 100644 --- a/tests/src/test/scala/cats/tests/ListTests.scala +++ b/tests/src/test/scala/cats/tests/ListTests.scala @@ -2,7 +2,7 @@ package cats package tests import cats.data.NonEmptyList -import cats.laws.discipline.{TraverseTests, CoflatMapTests, MonadCombineTests, SerializableTests, CartesianTests} +import cats.laws.discipline.{TraverseFilterTests, CoflatMapTests, MonadCombineTests, SerializableTests, CartesianTests} import cats.laws.discipline.arbitrary._ class ListTests extends CatsSuite { @@ -16,8 +16,8 @@ class ListTests extends CatsSuite { checkAll("List[Int]", MonadCombineTests[List].monadCombine[Int, Int, Int]) checkAll("MonadCombine[List]", SerializableTests.serializable(MonadCombine[List])) - checkAll("List[Int] with Option", TraverseTests[List].traverse[Int, Int, Int, List[Int], Option, Option]) - checkAll("Traverse[List]", SerializableTests.serializable(Traverse[List])) + checkAll("List[Int] with Option", TraverseFilterTests[List].traverseFilter[Int, Int, Int, List[Int], Option, Option]) + checkAll("TraverseFilter[List]", SerializableTests.serializable(TraverseFilter[List])) test("nel => list => nel returns original nel")( forAll { fa: NonEmptyList[Int] => diff --git a/tests/src/test/scala/cats/tests/ListWrapper.scala b/tests/src/test/scala/cats/tests/ListWrapper.scala index c00e74ae61..129c587986 100644 --- a/tests/src/test/scala/cats/tests/ListWrapper.scala +++ b/tests/src/test/scala/cats/tests/ListWrapper.scala @@ -44,27 +44,27 @@ object ListWrapper { def eqv[A : Eq]: Eq[ListWrapper[A]] = Eq[List[A]].on[ListWrapper[A]](_.list) - val traverse: Traverse[ListWrapper] = { - val F = Traverse[List] + val traverseFilter: TraverseFilter[ListWrapper] = { + val F = TraverseFilter[List] - new Traverse[ListWrapper] { + new TraverseFilter[ListWrapper] { def foldLeft[A, B](fa: ListWrapper[A], b: B)(f: (B, A) => B): B = F.foldLeft(fa.list, b)(f) def foldRight[A, B](fa: ListWrapper[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = F.foldRight(fa.list, lb)(f) - def traverse[G[_], A, B](fa: ListWrapper[A])(f: A => G[B])(implicit G0: Applicative[G]): G[ListWrapper[B]] = { - G0.map(F.traverse(fa.list)(f))(ListWrapper.apply) + def traverseFilter[G[_], A, B](fa: ListWrapper[A])(f: A => G[Option[B]])(implicit G0: Applicative[G]): G[ListWrapper[B]] = { + G0.map(F.traverseFilter(fa.list)(f))(ListWrapper.apply) } } } + val traverse: Traverse[ListWrapper] = traverseFilter + val foldable: Foldable[ListWrapper] = traverse - val functor: Functor[ListWrapper] = - new Functor[ListWrapper] { - def map[A, B](fa: ListWrapper[A])(f: A => B): ListWrapper[B] = - ListWrapper(Functor[List].map(fa.list)(f)) - } + val functor: Functor[ListWrapper] = traverse + + val functorFilter: FunctorFilter[ListWrapper] = traverseFilter val invariant: Invariant[ListWrapper] = functor diff --git a/tests/src/test/scala/cats/tests/NestedTests.scala b/tests/src/test/scala/cats/tests/NestedTests.scala index d2318b0860..9ad6bcac71 100644 --- a/tests/src/test/scala/cats/tests/NestedTests.scala +++ b/tests/src/test/scala/cats/tests/NestedTests.scala @@ -48,6 +48,13 @@ class NestedTests extends CatsSuite { checkAll("Functor[Nested[Option, ListWrapper, ?]]", SerializableTests.serializable(Functor[Nested[Option, ListWrapper, ?]])) } + { + // FunctorFilter composition + implicit val instance = ListWrapper.functorFilter + checkAll("Nested[List, ListWrapper, ?]", FunctorFilterTests[Nested[List, ListWrapper, ?]].functorFilter[Int, Int, Int]) + checkAll("FunctorFilter[Nested[List, ListWrapper, ?]]", SerializableTests.serializable(FunctorFilter[Nested[List, ListWrapper, ?]])) + } + { // Covariant + contravariant functor composition checkAll("Nested[Option, Show, ?]", ContravariantTests[Nested[Option, Show, ?]].contravariant[Int, Int, Int]) @@ -92,6 +99,20 @@ class NestedTests extends CatsSuite { checkAll("Foldable[Nested[List, ListWrapper, ?]]", SerializableTests.serializable(Foldable[Nested[List, ListWrapper, ?]])) } + { + // Traverse composition + implicit val instance = ListWrapper.traverse + checkAll("Nested[List, ListWrapper, ?]", TraverseTests[Nested[List, ListWrapper, ?]].traverse[Int, Int, Int, List[Int], Option, Option]) + checkAll("Traverse[Nested[List, ListWrapper, ?]]", SerializableTests.serializable(Traverse[Nested[List, ListWrapper, ?]])) + } + + { + // TraverseFilter composition + implicit val instance = ListWrapper.traverseFilter + checkAll("Nested[List, ListWrapper, ?]", TraverseFilterTests[Nested[List, ListWrapper, ?]].traverseFilter[Int, Int, Int, List[Int], Option, Option]) + checkAll("TraverseFilter[Nested[List, ListWrapper, ?]]", SerializableTests.serializable(TraverseFilter[Nested[List, ListWrapper, ?]])) + } + { // SI-2712? It can resolve Reducible[NonEmptyList] and Reducible[NonEmptyVector] but not // Reducible[Nested[NonEmptyList, NonEmptyVector, ?]] diff --git a/tests/src/test/scala/cats/tests/OptionTTests.scala b/tests/src/test/scala/cats/tests/OptionTTests.scala index dadb183637..392662d00f 100644 --- a/tests/src/test/scala/cats/tests/OptionTTests.scala +++ b/tests/src/test/scala/cats/tests/OptionTTests.scala @@ -39,8 +39,8 @@ class OptionTTests extends CatsSuite { // F has a Functor implicit val F = ListWrapper.functor - checkAll("OptionT[ListWrapper, Int]", FunctorTests[OptionT[ListWrapper, ?]].functor[Int, Int, Int]) - checkAll("Functor[OptionT[ListWrapper, ?]]", SerializableTests.serializable(Functor[OptionT[ListWrapper, ?]])) + checkAll("OptionT[ListWrapper, Int]", FunctorFilterTests[OptionT[ListWrapper, ?]].functorFilter[Int, Int, Int]) + checkAll("FunctorFilter[OptionT[ListWrapper, ?]]", SerializableTests.serializable(FunctorFilter[OptionT[ListWrapper, ?]])) } { @@ -122,11 +122,12 @@ class OptionTTests extends CatsSuite { // F has a Traverse implicit val F = ListWrapper.traverse - checkAll("OptionT[ListWrapper, Int] with Option", TraverseTests[OptionT[ListWrapper, ?]].traverse[Int, Int, Int, Int, Option, Option]) - checkAll("Traverse[OptionT[ListWrapper, ?]]", SerializableTests.serializable(Traverse[OptionT[ListWrapper, ?]])) + checkAll("OptionT[ListWrapper, Int] with Option", TraverseFilterTests[OptionT[ListWrapper, ?]].traverseFilter[Int, Int, Int, Int, Option, Option]) + checkAll("TraverseFilter[OptionT[ListWrapper, ?]]", SerializableTests.serializable(TraverseFilter[OptionT[ListWrapper, ?]])) Foldable[OptionT[ListWrapper, ?]] Functor[OptionT[ListWrapper, ?]] + Traverse[OptionT[ListWrapper, ?]] } { diff --git a/tests/src/test/scala/cats/tests/OptionTests.scala b/tests/src/test/scala/cats/tests/OptionTests.scala index a7e0d69d94..49308f36d9 100644 --- a/tests/src/test/scala/cats/tests/OptionTests.scala +++ b/tests/src/test/scala/cats/tests/OptionTests.scala @@ -17,8 +17,8 @@ class OptionTests extends CatsSuite { checkAll("Option[Int]", MonadRecTests[Option].monadRec[Int, Int, Int]) checkAll("MonadRec[Option]", SerializableTests.serializable(MonadRec[Option])) - checkAll("Option[Int] with Option", TraverseTests[Option].traverse[Int, Int, Int, Int, Option, Option]) - checkAll("Traverse[Option]", SerializableTests.serializable(Traverse[Option])) + checkAll("Option[Int] with Option", TraverseFilterTests[Option].traverseFilter[Int, Int, Int, Int, Option, Option]) + checkAll("TraverseFilter[Option]", SerializableTests.serializable(TraverseFilter[Option])) checkAll("Option with Unit", MonadErrorTests[Option, Unit].monadError[Int, Int, Int]) checkAll("MonadError[Option, Unit]", SerializableTests.serializable(MonadError[Option, Unit])) diff --git a/tests/src/test/scala/cats/tests/StreamTests.scala b/tests/src/test/scala/cats/tests/StreamTests.scala index 3e9de86445..36a064027d 100644 --- a/tests/src/test/scala/cats/tests/StreamTests.scala +++ b/tests/src/test/scala/cats/tests/StreamTests.scala @@ -1,7 +1,7 @@ package cats package tests -import cats.laws.discipline.{CoflatMapTests, MonadCombineTests, SerializableTests, TraverseTests, CartesianTests} +import cats.laws.discipline.{CoflatMapTests, MonadCombineTests, SerializableTests, TraverseFilterTests, CartesianTests} class StreamTests extends CatsSuite { checkAll("Stream[Int]", CartesianTests[Stream].cartesian[Int, Int, Int]) @@ -13,8 +13,8 @@ class StreamTests extends CatsSuite { checkAll("Stream[Int]", MonadCombineTests[Stream].monadCombine[Int, Int, Int]) checkAll("MonadCombine[Stream]", SerializableTests.serializable(MonadCombine[Stream])) - checkAll("Stream[Int] with Option", TraverseTests[Stream].traverse[Int, Int, Int, List[Int], Option, Option]) - checkAll("Traverse[Stream]", SerializableTests.serializable(Traverse[Stream])) + checkAll("Stream[Int] with Option", TraverseFilterTests[Stream].traverseFilter[Int, Int, Int, List[Int], Option, Option]) + checkAll("TraverseFilter[Stream]", SerializableTests.serializable(TraverseFilter[Stream])) test("show") { Stream(1, 2, 3).show should === ("Stream(1, ?)") diff --git a/tests/src/test/scala/cats/tests/VectorTests.scala b/tests/src/test/scala/cats/tests/VectorTests.scala index cb14ee0313..547cd90f8d 100644 --- a/tests/src/test/scala/cats/tests/VectorTests.scala +++ b/tests/src/test/scala/cats/tests/VectorTests.scala @@ -1,7 +1,7 @@ package cats package tests -import cats.laws.discipline.{MonadCombineTests, CoflatMapTests, SerializableTests, TraverseTests, CartesianTests} +import cats.laws.discipline.{MonadCombineTests, CoflatMapTests, SerializableTests, TraverseFilterTests, CartesianTests} class VectorTests extends CatsSuite { checkAll("Vector[Int]", CartesianTests[Vector].cartesian[Int, Int, Int]) @@ -13,8 +13,8 @@ class VectorTests extends CatsSuite { checkAll("Vector[Int]", MonadCombineTests[Vector].monadCombine[Int, Int, Int]) checkAll("MonadCombine[Vector]", SerializableTests.serializable(MonadCombine[Vector])) - checkAll("Vector[Int] with Option", TraverseTests[Vector].traverse[Int, Int, Int, List[Int], Option, Option]) - checkAll("Traverse[Vector]", SerializableTests.serializable(Traverse[Vector])) + checkAll("Vector[Int] with Option", TraverseFilterTests[Vector].traverseFilter[Int, Int, Int, List[Int], Option, Option]) + checkAll("TraverseFilter[Vector]", SerializableTests.serializable(TraverseFilter[Vector])) test("show") { Vector(1, 2, 3).show should === ("Vector(1, 2, 3)") From e04fc460a5aad528ba3702ec95763ca9775912b0 Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Sat, 23 Jul 2016 14:57:34 -0400 Subject: [PATCH 2/7] Add filter and collect tests for FunctorFilter --- core/src/main/scala/cats/data/Nested.scala | 6 ------ tests/src/test/scala/cats/tests/NestedTests.scala | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala/cats/data/Nested.scala b/core/src/main/scala/cats/data/Nested.scala index 9af568baf7..fc5fb50873 100644 --- a/core/src/main/scala/cats/data/Nested.scala +++ b/core/src/main/scala/cats/data/Nested.scala @@ -172,12 +172,6 @@ private[data] trait NestedTraverseFilter[F[_], G[_]] extends TraverseFilter[Nest override def traverseFilter[H[_]: Applicative, A, B](fga: Nested[F, G, A])(f: A => H[Option[B]]): H[Nested[F, G, B]] = Applicative[H].map(FG.traverseFilter(fga.value)(f))(Nested(_)) - - override def collect[A, B](fga: Nested[F, G, A])(f: PartialFunction[A, B]): Nested[F, G, B] = - Nested(FG.collect(fga.value)(f)) - - override def filter[A](fga: Nested[F, G, A])(f: A => Boolean): Nested[F, G, A] = - Nested(FG.filter(fga.value)(f)) } private[data] trait NestedApply[F[_], G[_]] extends Apply[Nested[F, G, ?]] with NestedFunctor[F, G] { diff --git a/tests/src/test/scala/cats/tests/NestedTests.scala b/tests/src/test/scala/cats/tests/NestedTests.scala index 9ad6bcac71..77152ef4de 100644 --- a/tests/src/test/scala/cats/tests/NestedTests.scala +++ b/tests/src/test/scala/cats/tests/NestedTests.scala @@ -53,6 +53,20 @@ class NestedTests extends CatsSuite { implicit val instance = ListWrapper.functorFilter checkAll("Nested[List, ListWrapper, ?]", FunctorFilterTests[Nested[List, ListWrapper, ?]].functorFilter[Int, Int, Int]) checkAll("FunctorFilter[Nested[List, ListWrapper, ?]]", SerializableTests.serializable(FunctorFilter[Nested[List, ListWrapper, ?]])) + + test("collect consistency") { + forAll { l: Nested[List, ListWrapper, Int] => + val even: PartialFunction[Int, Int] = { case i if i % 2 == 0 => i } + l.collect(even).value should === (l.value.map(_.collect(even))) + } + } + + test("filter consistency") { + forAll { l: Nested[List, ListWrapper, Int] => + def even(i: Int): Boolean = i % 2 == 0 + l.filter(even).value should === (l.value.map(_.filter(even))) + } + } } { From b0af1d694c611225eb4a922127fe5e5a10538c5b Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Sat, 23 Jul 2016 16:05:19 -0400 Subject: [PATCH 3/7] Improve FunctorFilter/TraverseFilter tests --- .../scala/cats/laws/discipline/TraverseFilterTests.scala | 4 ---- tests/src/test/scala/cats/tests/CatsSuite.scala | 4 ++++ tests/src/test/scala/cats/tests/NestedTests.scala | 4 +--- tests/src/test/scala/cats/tests/StreamTests.scala | 6 ++++++ tests/src/test/scala/cats/tests/VectorTests.scala | 5 +++++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala b/laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala index a7a0ee5b38..6834c57b73 100644 --- a/laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala @@ -24,10 +24,6 @@ trait TraverseFilterTests[F[_]] extends TraverseTests[F] with FunctorFilterTests EqXFB: Eq[X[F[B]]], EqYFB: Eq[Y[F[B]]] ): RuleSet = { - implicit def EqXFBYFB : Eq[(X[F[B]], Y[F[B]])] = new Eq[(X[F[B]], Y[F[B]])] { - override def eqv(x: (X[F[B]], Y[F[B]]), y: (X[F[B]], Y[F[B]])): Boolean = - EqXFB.eqv(x._1, y._1) && EqYFB.eqv(x._2, y._2) - } new RuleSet { def name: String = "collect" def bases: Seq[(String, RuleSet)] = Nil diff --git a/tests/src/test/scala/cats/tests/CatsSuite.scala b/tests/src/test/scala/cats/tests/CatsSuite.scala index ac72c7fc68..8341ef36c6 100644 --- a/tests/src/test/scala/cats/tests/CatsSuite.scala +++ b/tests/src/test/scala/cats/tests/CatsSuite.scala @@ -42,6 +42,10 @@ trait CatsSuite extends FunSuite with Matchers with GeneratorDrivenPropertyCheck // disable Eq syntax (by making `catsSyntaxEq` not implicit), since it collides // with scalactic's equality override def catsSyntaxEq[A: Eq](a: A): EqOps[A] = new EqOps[A](a) + + def even(i: Int): Boolean = i % 2 == 0 + + val evenPf: PartialFunction[Int, Int] = { case i if even(i) => i } } trait SlowCatsSuite extends CatsSuite { diff --git a/tests/src/test/scala/cats/tests/NestedTests.scala b/tests/src/test/scala/cats/tests/NestedTests.scala index 77152ef4de..e4d838b5c4 100644 --- a/tests/src/test/scala/cats/tests/NestedTests.scala +++ b/tests/src/test/scala/cats/tests/NestedTests.scala @@ -56,14 +56,12 @@ class NestedTests extends CatsSuite { test("collect consistency") { forAll { l: Nested[List, ListWrapper, Int] => - val even: PartialFunction[Int, Int] = { case i if i % 2 == 0 => i } - l.collect(even).value should === (l.value.map(_.collect(even))) + l.collect(evenPf).value should === (l.value.map(_.collect(evenPf))) } } test("filter consistency") { forAll { l: Nested[List, ListWrapper, Int] => - def even(i: Int): Boolean = i % 2 == 0 l.filter(even).value should === (l.value.map(_.filter(even))) } } diff --git a/tests/src/test/scala/cats/tests/StreamTests.scala b/tests/src/test/scala/cats/tests/StreamTests.scala index 36a064027d..0fec800c77 100644 --- a/tests/src/test/scala/cats/tests/StreamTests.scala +++ b/tests/src/test/scala/cats/tests/StreamTests.scala @@ -37,4 +37,10 @@ class StreamTests extends CatsSuite { } } } + + test("collect consistency") { + forAll { s: Stream[Int] => + FunctorFilter[Stream].collect(s)(evenPf) should === (s.collect(evenPf)) + } + } } diff --git a/tests/src/test/scala/cats/tests/VectorTests.scala b/tests/src/test/scala/cats/tests/VectorTests.scala index 547cd90f8d..70598e2c5b 100644 --- a/tests/src/test/scala/cats/tests/VectorTests.scala +++ b/tests/src/test/scala/cats/tests/VectorTests.scala @@ -26,4 +26,9 @@ class VectorTests extends CatsSuite { } } + test("collect consistency") { + forAll { vec: Vector[Int] => + FunctorFilter[Vector].collect(vec)(evenPf) should === (vec.collect(evenPf)) + } + } } From c4b55caad0920fe9f1503a1b1bb543017814a475 Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Sat, 23 Jul 2016 16:16:29 -0400 Subject: [PATCH 4/7] Use Filter in Functor/TraverseFilter implicit names --- core/src/main/scala/cats/data/Const.scala | 2 +- core/src/main/scala/cats/data/OptionT.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/cats/data/Const.scala b/core/src/main/scala/cats/data/Const.scala index 227cd6e685..65b0132ce0 100644 --- a/core/src/main/scala/cats/data/Const.scala +++ b/core/src/main/scala/cats/data/Const.scala @@ -56,7 +56,7 @@ private[data] sealed abstract class ConstInstances extends ConstInstances0 { fa.retag[B] } - implicit def catsDataTraverseForConst[C]: TraverseFilter[Const[C, ?]] = new TraverseFilter[Const[C, ?]] { + implicit def catsDataTraverseFilterForConst[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) diff --git a/core/src/main/scala/cats/data/OptionT.scala b/core/src/main/scala/cats/data/OptionT.scala index 2921848a03..0d02976e95 100644 --- a/core/src/main/scala/cats/data/OptionT.scala +++ b/core/src/main/scala/cats/data/OptionT.scala @@ -230,7 +230,7 @@ private[data] sealed trait OptionTInstances2 extends OptionTInstances3 { } private[data] sealed trait OptionTInstances3 { - implicit def catsDataFunctorForOptionT[F[_]](implicit F0: Functor[F]): FunctorFilter[OptionT[F, ?]] = + implicit def catsDataFunctorFilterForOptionT[F[_]](implicit F0: Functor[F]): FunctorFilter[OptionT[F, ?]] = new OptionTFunctor[F] { implicit val F = F0 } } From 7e5b0f4317d410b4d37e8af92b9fca4ca0ec0dac Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Sat, 23 Jul 2016 16:36:15 -0400 Subject: [PATCH 5/7] Fix Const and OptionT tests --- tests/src/test/scala/cats/tests/ConstTests.scala | 2 +- tests/src/test/scala/cats/tests/OptionTTests.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/src/test/scala/cats/tests/ConstTests.scala b/tests/src/test/scala/cats/tests/ConstTests.scala index e485b5482a..c8c7581d29 100644 --- a/tests/src/test/scala/cats/tests/ConstTests.scala +++ b/tests/src/test/scala/cats/tests/ConstTests.scala @@ -10,7 +10,7 @@ import cats.laws.discipline.arbitrary.{catsLawsArbitraryForConst, catsLawsArbitr class ConstTests extends CatsSuite { - implicit val iso = CartesianTests.Isomorphisms.invariant[Const[String, ?]](Const.catsDataTraverseForConst) + implicit val iso = CartesianTests.Isomorphisms.invariant[Const[String, ?]](Const.catsDataTraverseFilterForConst) checkAll("Const[String, Int]", CartesianTests[Const[String, ?]].cartesian[Int, Int, Int]) checkAll("Cartesian[Const[String, ?]]", SerializableTests.serializable(Cartesian[Const[String, ?]])) diff --git a/tests/src/test/scala/cats/tests/OptionTTests.scala b/tests/src/test/scala/cats/tests/OptionTTests.scala index 392662d00f..6d3dd71fd1 100644 --- a/tests/src/test/scala/cats/tests/OptionTTests.scala +++ b/tests/src/test/scala/cats/tests/OptionTTests.scala @@ -7,7 +7,7 @@ import cats.laws.discipline._ import cats.laws.discipline.arbitrary._ class OptionTTests extends CatsSuite { - implicit val iso = CartesianTests.Isomorphisms.invariant[OptionT[ListWrapper, ?]](OptionT.catsDataFunctorForOptionT(ListWrapper.functor)) + implicit val iso = CartesianTests.Isomorphisms.invariant[OptionT[ListWrapper, ?]](OptionT.catsDataFunctorFilterForOptionT(ListWrapper.functor)) { implicit val F = ListWrapper.eqv[Option[Int]] From 947594cf27bc71fcff050ea0972dc2d3ffd1ace6 Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Sun, 24 Jul 2016 07:55:12 -0400 Subject: [PATCH 6/7] Fix minor typos based on #1225 review --- core/src/main/scala/cats/FunctorFilter.scala | 2 +- .../main/scala/cats/laws/discipline/TraverseFilterTests.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/cats/FunctorFilter.scala b/core/src/main/scala/cats/FunctorFilter.scala index 921dd70b67..cbbb5755de 100644 --- a/core/src/main/scala/cats/FunctorFilter.scala +++ b/core/src/main/scala/cats/FunctorFilter.scala @@ -29,7 +29,7 @@ import simulacrum.typeclass * {{{ * scala> import cats.implicits._ * scala> val l: List[Int] = List(1, 2, 3, 4) - * scala> TraverseFilter[List].collect(l){ + * scala> FunctorFilter[List].collect(l){ * | case 1 => "one" * | case 3 => "three" * | } diff --git a/laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala b/laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala index 6834c57b73..f1e717e1bf 100644 --- a/laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/TraverseFilterTests.scala @@ -25,7 +25,7 @@ trait TraverseFilterTests[F[_]] extends TraverseTests[F] with FunctorFilterTests EqYFB: Eq[Y[F[B]]] ): RuleSet = { new RuleSet { - def name: String = "collect" + def name: String = "traverseFilter" def bases: Seq[(String, RuleSet)] = Nil def parents: Seq[RuleSet] = Seq(traverse[A, B, C, M, X, Y], functorFilter[A, B, C]) def props: Seq[(String, Prop)] = Seq( From bf12418aba7113d08528212371749f6b42672793 Mon Sep 17 00:00:00 2001 From: Cody Allen Date: Sun, 24 Jul 2016 08:20:26 -0400 Subject: [PATCH 7/7] Add TraverseFilter instance for Map --- core/src/main/scala/cats/instances/map.scala | 14 +++++++++++--- tests/src/test/scala/cats/tests/MapTests.scala | 6 +++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/core/src/main/scala/cats/instances/map.scala b/core/src/main/scala/cats/instances/map.scala index ca525be5f8..a03a1e0e8f 100644 --- a/core/src/main/scala/cats/instances/map.scala +++ b/core/src/main/scala/cats/instances/map.scala @@ -11,10 +11,10 @@ trait MapInstances extends cats.kernel.instances.MapInstances { s"Map($body)" } - implicit def catsStdInstancesForMap[K]: Traverse[Map[K, ?]] with FlatMap[Map[K, ?]] = - new Traverse[Map[K, ?]] with FlatMap[Map[K, ?]] { + implicit def catsStdInstancesForMap[K]: TraverseFilter[Map[K, ?]] with FlatMap[Map[K, ?]] = + new TraverseFilter[Map[K, ?]] with FlatMap[Map[K, ?]] { - def traverse[G[_], A, B](fa: Map[K, A])(f: (A) => G[B])(implicit G: Applicative[G]): G[Map[K, B]] = { + override def traverse[G[_], A, B](fa: Map[K, A])(f: A => G[B])(implicit G: Applicative[G]): G[Map[K, B]] = { val gba: Eval[G[Map[K, B]]] = Always(G.pure(Map.empty)) val gbb = Foldable.iterateRight(fa.iterator, gba){ (kv, lbuf) => G.map2Eval(f(kv._2), lbuf)({ (b, buf) => buf + (kv._1 -> b)}) @@ -22,6 +22,14 @@ trait MapInstances extends cats.kernel.instances.MapInstances { G.map(gbb)(_.toMap) } + def traverseFilter[G[_], A, B](fa: Map[K, A])(f: A => G[Option[B]])(implicit G: Applicative[G]): G[Map[K, B]] = { + val gba: Eval[G[Map[K, B]]] = Always(G.pure(Map.empty)) + val gbb = Foldable.iterateRight(fa.iterator, gba){ (kv, lbuf) => + G.map2Eval(f(kv._2), lbuf)({ (ob, buf) => ob.fold(buf)(b => buf + (kv._1 -> b))}) + }.value + G.map(gbb)(_.toMap) + } + override def map[A, B](fa: Map[K, A])(f: A => B): Map[K, B] = fa.map { case (k, a) => (k, f(a)) } diff --git a/tests/src/test/scala/cats/tests/MapTests.scala b/tests/src/test/scala/cats/tests/MapTests.scala index fb33354c64..e83d1d4c2d 100644 --- a/tests/src/test/scala/cats/tests/MapTests.scala +++ b/tests/src/test/scala/cats/tests/MapTests.scala @@ -1,7 +1,7 @@ package cats package tests -import cats.laws.discipline.{TraverseTests, FlatMapTests, SerializableTests, CartesianTests} +import cats.laws.discipline.{TraverseFilterTests, FlatMapTests, SerializableTests, CartesianTests} class MapTests extends CatsSuite { implicit val iso = CartesianTests.Isomorphisms.invariant[Map[Int, ?]] @@ -12,8 +12,8 @@ class MapTests extends CatsSuite { checkAll("Map[Int, Int]", FlatMapTests[Map[Int, ?]].flatMap[Int, Int, Int]) checkAll("FlatMap[Map[Int, ?]]", SerializableTests.serializable(FlatMap[Map[Int, ?]])) - checkAll("Map[Int, Int] with Option", TraverseTests[Map[Int, ?]].traverse[Int, Int, Int, Int, Option, Option]) - checkAll("Traverse[Map[Int, ?]]", SerializableTests.serializable(Traverse[Map[Int, ?]])) + checkAll("Map[Int, Int] with Option", TraverseFilterTests[Map[Int, ?]].traverseFilter[Int, Int, Int, Int, Option, Option]) + checkAll("TraverseFilter[Map[Int, ?]]", SerializableTests.serializable(TraverseFilter[Map[Int, ?]])) test("show isn't empty and is formatted as expected") { forAll { (map: Map[Int, String]) =>