From a041a9429ff6d08c9af4f0139987f9dda6492e99 Mon Sep 17 00:00:00 2001 From: Eugene Platonov Date: Wed, 3 Apr 2024 22:17:54 -0400 Subject: [PATCH] Add `mapOrKeep` to Functor --- core/src/main/scala/cats/Functor.scala | 14 +++++++++++++ docs/nomenclature.md | 21 ++++++++++--------- .../main/scala/cats/laws/FunctorLaws.scala | 9 ++++++++ .../cats/laws/discipline/FunctorTests.scala | 5 ++++- .../test/scala/cats/tests/FunctorSuite.scala | 12 +++++++++++ 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/core/src/main/scala/cats/Functor.scala b/core/src/main/scala/cats/Functor.scala index 5109a21b53c..7707186e8b7 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, identity[A1])) + /** * 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/laws/src/main/scala/cats/laws/FunctorLaws.scala b/laws/src/main/scala/cats/laws/FunctorLaws.scala index 3a7c539a1f7..21b2166f277 100644 --- a/laws/src/main/scala/cats/laws/FunctorLaws.scala +++ b/laws/src/main/scala/cats/laws/FunctorLaws.scala @@ -33,6 +33,15 @@ trait FunctorLaws[F[_]] extends InvariantLaws[F] { def covariantIdentity[A](fa: F[A]): IsEq[F[A]] = fa.map(identity) <-> fa + def mapOrKeepEmpty[A](fa: F[A]): IsEq[F[A]] = + fa.mapOrKeep(PartialFunction.empty) <-> fa + + def mapOrKeepIdentity[A](fa: F[A]): IsEq[F[A]] = + fa.mapOrKeep { case a => a } <-> fa + + def mapOrKeepWiden[A, A1 >: A](fa: F[A]): IsEq[F[A1]] = + fa.mapOrKeep { case a => a: A1 } <-> fa.widen[A1] + def covariantComposition[A, B, C](fa: F[A], f: A => B, g: B => C): IsEq[F[C]] = fa.map(f).map(g) <-> fa.map(f.andThen(g)) } diff --git a/laws/src/main/scala/cats/laws/discipline/FunctorTests.scala b/laws/src/main/scala/cats/laws/discipline/FunctorTests.scala index f27fc7e7e6f..1ee6619b35d 100644 --- a/laws/src/main/scala/cats/laws/discipline/FunctorTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/FunctorTests.scala @@ -41,7 +41,10 @@ trait FunctorTests[F[_]] extends InvariantTests[F] { name = "functor", parent = Some(invariant[A, B, C]), "covariant identity" -> forAll(laws.covariantIdentity[A] _), - "covariant composition" -> forAll(laws.covariantComposition[A, B, C] _) + "covariant composition" -> forAll(laws.covariantComposition[A, B, C] _), + "mapOrKeep empty" -> forAll(laws.mapOrKeepEmpty[A] _), + "mapOrKeep identity" -> forAll(laws.mapOrKeepIdentity[A] _), + "mapOrKeep widen" -> forAll(laws.mapOrKeepWiden[A, A] _) ) } 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)))))