diff --git a/core/src/main/scala/cats/Functor.scala b/core/src/main/scala/cats/Functor.scala index 5109a21b53c..852abb982d0 100644 --- a/core/src/main/scala/cats/Functor.scala +++ b/core/src/main/scala/cats/Functor.scala @@ -177,6 +177,19 @@ trait Functor[F[_]] extends Invariant[F] { self => */ def tupleRight[A, B](fa: F[A], b: B): F[(A, B)] = map(fa)(a => (a, b)) + /** + * Modifies the `A` value in `F[A]` with the supplied function, if the function is defined for the value. + * Example: + * {{{ + * scala> import cats.Functor + * scala> import cats.implicits.catsStdInstancesForList + * + * scala> Functor[List].mapOrKeep(List(1, 2, 3)) { case 2 => 42 } + * res0: List[Int] = List(1, 42, 3) + * }}} + */ + def mapOrKeep[A, A1 >: A](fa: F[A])(pf: PartialFunction[A, A1]): F[A1] = map(fa)(a => pf.applyOrElse(a, _ => a)) + /** * Un-zips an `F[(A, B)]` consisting of element pairs or Tuple2 into two separate F's tupled. * @@ -258,6 +271,7 @@ object Functor { def as[B](b: B): F[B] = typeClassInstance.as[A, B](self, b) def tupleLeft[B](b: B): F[(B, A)] = typeClassInstance.tupleLeft[A, B](self, b) def tupleRight[B](b: B): F[(A, B)] = typeClassInstance.tupleRight[A, B](self, b) + def mapOrKeep[A1 >: A](pf: PartialFunction[A, A1]): F[A1] = typeClassInstance.mapOrKeep[A, A1](self)(pf) } trait AllOps[F[_], A] extends Ops[F, A] with Invariant.AllOps[F, A] { type TypeClassType <: Functor[F] diff --git a/docs/nomenclature.md b/docs/nomenclature.md index 5b8c37dcfdc..eec6e1c1145 100644 --- a/docs/nomenclature.md +++ b/docs/nomenclature.md @@ -14,16 +14,17 @@ _WARNING_: this page is written manually, and not automatically generated, so ma ### Functor -| Type | Method Name | -| ------------- |--------------| -| `F[A] => F[Unit]` | `void` | -| `F[A] => B => F[B]` | `as` | -| `F[A] => (A => B) => F[B]` | `map` | -| `F[A] => (A => B) => F[(A,B)]` | `fproduct` | -| `F[A] => (A => B) => F[(B,A)]` | `fproductLeft` | -| `F[A] => B => F[(B, A)]` | `tupleLeft` | -| `F[A] => B => F[(A, B)]` | `tupleRight` | -| `(A => B) => (F[A] => F[B])` | `lift` | +| Type | Method Name | Notes | +|--------------------------------|----------------|-------| +| `F[A] => F[Unit]` | `void` | +| `F[A] => B => F[B]` | `as` | +| `F[A] => (A => B) => F[B]` | `map` | +| `F[A] => (A => A1) => F[A1])` | `mapOrKeep` | A1 >: A, the (A => A1) is a PartialFunction +| `F[A] => (A => B) => F[(A,B)]` | `fproduct` | +| `F[A] => (A => B) => F[(B,A)]` | `fproductLeft` | +| `F[A] => B => F[(B, A)]` | `tupleLeft` | +| `F[A] => B => F[(A, B)]` | `tupleRight` | +| `(A => B) => (F[A] => F[B])` | `lift` | ### Apply diff --git a/tests/shared/src/test/scala/cats/tests/FunctorSuite.scala b/tests/shared/src/test/scala/cats/tests/FunctorSuite.scala index 1c22cc08701..a1850cfae3b 100644 --- a/tests/shared/src/test/scala/cats/tests/FunctorSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/FunctorSuite.scala @@ -45,6 +45,18 @@ class FunctorSuite extends CatsSuite { } } + test("mapOrKeep maps matching elements preserving structure") { + forAll { (l: List[Int], o: Option[Int], m: Map[String, Int]) => + assert(l.mapOrKeep { case i if i % 2 == 0 => i + 1 } === l.map(i => if (i % 2 == 0) i + 1 else i)) + assert( + o.mapOrKeep { case i if i > 0 => i + 1 } === (if (o.nonEmpty) if (o.get > 0) Some(o.get + 1) else o else None) + ) + assert( + m.mapOrKeep { case v if v % 2 == 0 => v + 1 } === m.map { case (k, v) => k -> (if (v % 2 == 0) v + 1 else v) } + ) + } + } + test("tupleLeft and tupleRight tuple values with a constant value preserving structure") { forAll { (l: List[Int], o: Option[Int], m: Map[String, Int], i: Int) => assert(l.tupleLeft(i) === (List.tabulate(l.length)(in => (i, l(in)))))