From 3f19e233f165ead40cc8c902f6415a1738cec51a Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Tue, 25 Feb 2020 02:33:39 +0800 Subject: [PATCH 01/16] Drop noop for Functor unzip with Liskov evidence --- core/src/main/scala/cats/Functor.scala | 10 ++++--- .../test/scala/cats/tests/FunctorSuite.scala | 26 ++++++++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/cats/Functor.scala b/core/src/main/scala/cats/Functor.scala index cbc3feb559..6a3b596d2e 100644 --- a/core/src/main/scala/cats/Functor.scala +++ b/core/src/main/scala/cats/Functor.scala @@ -1,6 +1,7 @@ package cats -import simulacrum.{noop, typeclass} +import cats.evidence.<~< +import simulacrum.typeclass /** * Functor. @@ -158,8 +159,11 @@ import simulacrum.{noop, typeclass} * }}} * */ - @noop - def unzip[A, B](fab: F[(A, B)]): (F[A], F[B]) = (map(fab)(_._1), map(fab)(_._2)) + def unzip[A, X, Y](fa: F[A])(implicit ev: A <~< (X, Y)): (F[X], F[Y]) = { + val fxy = map(fa)(ev) + + (map(fxy)(_._1), map(fxy)(_._2)) + } /** * Lifts `if` to Functor diff --git a/tests/src/test/scala/cats/tests/FunctorSuite.scala b/tests/src/test/scala/cats/tests/FunctorSuite.scala index ea04d16912..0ba64885d9 100644 --- a/tests/src/test/scala/cats/tests/FunctorSuite.scala +++ b/tests/src/test/scala/cats/tests/FunctorSuite.scala @@ -1,6 +1,10 @@ package cats package tests +import cats.data.{NonEmptyList, NonEmptyMap} +import cats.laws.discipline.arbitrary._ +import org.scalatest.matchers.must.Matchers._ + class FunctorSuite extends CatsSuite { test("void replaces values with unit preserving structure") { forAll { (l: List[Int], o: Option[Int], m: Map[String, Int]) => @@ -30,11 +34,31 @@ class FunctorSuite extends CatsSuite { } test("unzip preserves structure") { - forAll { (l: List[Int], o: Option[Int], m: Map[String, Int]) => + forAll { (nel: NonEmptyList[Int], o: Option[Int], nem: NonEmptyMap[String, Int]) => + val l = nel.toList + val m = nem.toSortedMap + Functor[List].unzip(l.map(i => (i, i))) === ((l, l)) Functor[Option].unzip(o.map(i => (i, i))) === ((o, o)) Functor[Map[String, *]].unzip(m.map { case (k, v) => (k, (v, v)) }) === ((m, m)) + + //postfix test for Cats datatypes + nel.map(i => (i, i)).unzip === ((nel, nel)) + nem.map(v => (v, v)).unzip === ((nem, nem)) } + + //empty test for completeness + val emptyL = List.empty[Int] + val emptyM = Map.empty[String, Int] + + Functor[List].unzip(List.empty[(Int, Int)]) === ((emptyL, emptyL)) + Functor[Map[String, *]].unzip(Map.empty[String, (Int, Int)]) === ((emptyM, emptyM)) + } + + test("unzip only typechecks for Tuple2") { + "(NonEmptyList one 1).unzip" mustNot typeCheck + "(NonEmptyList one ((1, 2))).unzip" must compile + "(NonEmptyList one ((1, 2, 3))).unzip" mustNot typeCheck } test("widen equals map(identity)") { From 35a5f973f970b0527b94c780f6cbde2f1a9877c6 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Tue, 25 Feb 2020 23:52:39 +0800 Subject: [PATCH 02/16] Overload unzip for bin compat --- core/src/main/scala/cats/Functor.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/src/main/scala/cats/Functor.scala b/core/src/main/scala/cats/Functor.scala index 6a3b596d2e..f07718cfa9 100644 --- a/core/src/main/scala/cats/Functor.scala +++ b/core/src/main/scala/cats/Functor.scala @@ -1,7 +1,7 @@ package cats import cats.evidence.<~< -import simulacrum.typeclass +import simulacrum.{noop, typeclass} /** * Functor. @@ -159,10 +159,16 @@ import simulacrum.typeclass * }}} * */ - def unzip[A, X, Y](fa: F[A])(implicit ev: A <~< (X, Y)): (F[X], F[Y]) = { + //TODO: Drop from Cats 3.0 + @noop + def unzip[A, B](fab: F[(A, B)]): (F[A], F[B]) = (map(fab)(_._1), map(fab)(_._2)) + + //TODO: Drop `final` when the `noop` version deprecates + // `final` restricts functor instance overriding of `unzip` to the noop version + final def unzip[A, X, Y](fa: F[A])(implicit ev: A <~< (X, Y)): (F[X], F[Y]) = { val fxy = map(fa)(ev) - (map(fxy)(_._1), map(fxy)(_._2)) + unzip(fxy) } /** From 1cac3498202dadd6cd9c33f55f618d60954bbdf4 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Wed, 26 Feb 2020 01:42:39 +0800 Subject: [PATCH 03/16] Make type params explicit --- core/src/main/scala/cats/Functor.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/Functor.scala b/core/src/main/scala/cats/Functor.scala index f07718cfa9..56a6d37a99 100644 --- a/core/src/main/scala/cats/Functor.scala +++ b/core/src/main/scala/cats/Functor.scala @@ -168,7 +168,7 @@ import simulacrum.{noop, typeclass} final def unzip[A, X, Y](fa: F[A])(implicit ev: A <~< (X, Y)): (F[X], F[Y]) = { val fxy = map(fa)(ev) - unzip(fxy) + unzip[X, Y](fxy) } /** From 999f7359664d9c3a5ed33dae120ff291ac7d6b2a Mon Sep 17 00:00:00 2001 From: Gagandeep kalra Date: Tue, 7 Jan 2020 21:54:29 +0800 Subject: [PATCH 04/16] Backport unzip method in functor typeclass for scala_2.11 (#3234) * backported unzip method in functor typeclass * fixed nonfunctioning doctest --- core/src/main/scala/cats/implicits.scala | 1 + core/src/main/scala/cats/syntax/all.scala | 3 +++ core/src/main/scala/cats/syntax/functor.scala | 26 +++++++++++++++++++ core/src/main/scala/cats/syntax/package.scala | 2 +- .../src/test/scala/cats/tests/CatsSuite.scala | 1 + .../test/scala/cats/tests/FunctorSuite.scala | 10 +++++++ 6 files changed, 42 insertions(+), 1 deletion(-) diff --git a/core/src/main/scala/cats/implicits.scala b/core/src/main/scala/cats/implicits.scala index 5c2f3202b9..03b246f075 100644 --- a/core/src/main/scala/cats/implicits.scala +++ b/core/src/main/scala/cats/implicits.scala @@ -9,6 +9,7 @@ object implicits with syntax.AllSyntaxBinCompat4 with syntax.AllSyntaxBinCompat5 with syntax.AllSyntaxBinCompat6 + with syntax.AllSyntaxBinCompat7 with instances.AllInstances with instances.AllInstancesBinCompat0 with instances.AllInstancesBinCompat1 diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index c19ea72e45..b66c7957f5 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -10,6 +10,7 @@ abstract class AllSyntaxBinCompat with AllSyntaxBinCompat4 with AllSyntaxBinCompat5 with AllSyntaxBinCompat6 + with AllSyntaxBinCompat7 trait AllSyntax extends AlternativeSyntax @@ -95,3 +96,5 @@ trait AllSyntaxBinCompat4 trait AllSyntaxBinCompat5 extends ParallelBitraverseSyntax trait AllSyntaxBinCompat6 extends ParallelUnorderedTraverseSyntax + +trait AllSyntaxBinCompat7 extends FunctorSyntaxBinCompat0 diff --git a/core/src/main/scala/cats/syntax/functor.scala b/core/src/main/scala/cats/syntax/functor.scala index fcb9d1ce46..d0b5100f69 100644 --- a/core/src/main/scala/cats/syntax/functor.scala +++ b/core/src/main/scala/cats/syntax/functor.scala @@ -1,4 +1,30 @@ package cats package syntax +import scala.language.implicitConversions + trait FunctorSyntax extends Functor.ToFunctorOps + +private[syntax] trait FunctorSyntaxBinCompat0 { + implicit final def catsSyntaxUnzipFunctorOps[F[_], A, B](fa: F[(A, B)]): UnzipFunctorOps[F, A, B] = + new UnzipFunctorOps[F, A, B](fa) +} + +final class UnzipFunctorOps[F[_], A, B](private val fab: F[(A, B)]) extends AnyVal { + + /** + * Un-zips an `F[(A, B)]` consisting of element pairs or Tuple2 into two separate F's tupled. + * + * NOTE: Check for effect duplication, possibly memoize before + * + * {{{ + * scala> import cats.Id + * scala> import cats.syntax.functor._ + * + * scala> (5: Id[Int]).map(i => (i, i)).unzip == ((5, 5)) + * res0: Boolean = true + * }}} + * + */ + def unzip(implicit F: Functor[F]): (F[A], F[B]) = (F.map(fab)(_._1), F.map(fab)(_._2)) +} diff --git a/core/src/main/scala/cats/syntax/package.scala b/core/src/main/scala/cats/syntax/package.scala index 022b6a5f5c..ddca7fcac2 100644 --- a/core/src/main/scala/cats/syntax/package.scala +++ b/core/src/main/scala/cats/syntax/package.scala @@ -28,7 +28,7 @@ package object syntax { object eq extends EqSyntax object flatMap extends FlatMapSyntax with FlatMapOptionSyntax object foldable extends FoldableSyntax with FoldableSyntaxBinCompat0 with FoldableSyntaxBinCompat1 - object functor extends FunctorSyntax + object functor extends FunctorSyntax with FunctorSyntaxBinCompat0 object functorFilter extends FunctorFilterSyntax object group extends GroupSyntax object hash extends HashSyntax diff --git a/tests/src/test/scala/cats/tests/CatsSuite.scala b/tests/src/test/scala/cats/tests/CatsSuite.scala index 9c90dc2f2a..f2c5b1b560 100644 --- a/tests/src/test/scala/cats/tests/CatsSuite.scala +++ b/tests/src/test/scala/cats/tests/CatsSuite.scala @@ -53,6 +53,7 @@ trait CatsSuite with AllSyntaxBinCompat4 with AllSyntaxBinCompat5 with AllSyntaxBinCompat6 + with AllSyntaxBinCompat7 with StrictCatsEquality { implicit override val generatorDrivenConfig: PropertyCheckConfiguration = diff --git a/tests/src/test/scala/cats/tests/FunctorSuite.scala b/tests/src/test/scala/cats/tests/FunctorSuite.scala index 0ba64885d9..00da5f81ca 100644 --- a/tests/src/test/scala/cats/tests/FunctorSuite.scala +++ b/tests/src/test/scala/cats/tests/FunctorSuite.scala @@ -86,4 +86,14 @@ class FunctorSuite extends CatsSuite { } + test("unzip preserves structure") { + forAll { (l: List[Int], o: Option[Int], m: Map[String, Int]) => + def doUnzip[F[_]: Functor, A, B](fab: F[(A, B)]): (F[A], F[B]) = fab.unzip + + doUnzip(l.map(i => (i, i))) === ((l, l)) + doUnzip(o.map(i => (i, i))) === ((o, o)) + doUnzip(m.map { case (k, v) => (k, (v, v)) }) === ((m, m)) + } + } + } From 08c1ad870bf87daad5fa02b724fd5a1650c67834 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Mon, 9 Mar 2020 03:08:35 +0800 Subject: [PATCH 05/16] Clean up cherry-picked forwardport --- core/src/main/scala/cats/syntax/functor.scala | 2 -- tests/src/test/scala/cats/tests/FunctorSuite.scala | 10 ---------- 2 files changed, 12 deletions(-) diff --git a/core/src/main/scala/cats/syntax/functor.scala b/core/src/main/scala/cats/syntax/functor.scala index d0b5100f69..edf22d762b 100644 --- a/core/src/main/scala/cats/syntax/functor.scala +++ b/core/src/main/scala/cats/syntax/functor.scala @@ -1,8 +1,6 @@ package cats package syntax -import scala.language.implicitConversions - trait FunctorSyntax extends Functor.ToFunctorOps private[syntax] trait FunctorSyntaxBinCompat0 { diff --git a/tests/src/test/scala/cats/tests/FunctorSuite.scala b/tests/src/test/scala/cats/tests/FunctorSuite.scala index 00da5f81ca..0ba64885d9 100644 --- a/tests/src/test/scala/cats/tests/FunctorSuite.scala +++ b/tests/src/test/scala/cats/tests/FunctorSuite.scala @@ -86,14 +86,4 @@ class FunctorSuite extends CatsSuite { } - test("unzip preserves structure") { - forAll { (l: List[Int], o: Option[Int], m: Map[String, Int]) => - def doUnzip[F[_]: Functor, A, B](fab: F[(A, B)]): (F[A], F[B]) = fab.unzip - - doUnzip(l.map(i => (i, i))) === ((l, l)) - doUnzip(o.map(i => (i, i))) === ((o, o)) - doUnzip(m.map { case (k, v) => (k, (v, v)) }) === ((m, m)) - } - } - } From 47312b446deab1180b60e15d571f8e3fbf4f80f5 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Tue, 10 Mar 2020 03:13:14 +0800 Subject: [PATCH 06/16] Replace UnzipFunctorOps with FunctorOps0 with Liskov based unzip implementation --- core/src/main/scala/cats/Functor.scala | 10 ---------- core/src/main/scala/cats/syntax/functor.scala | 14 ++++++++++---- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/core/src/main/scala/cats/Functor.scala b/core/src/main/scala/cats/Functor.scala index 56a6d37a99..cbc3feb559 100644 --- a/core/src/main/scala/cats/Functor.scala +++ b/core/src/main/scala/cats/Functor.scala @@ -1,6 +1,5 @@ package cats -import cats.evidence.<~< import simulacrum.{noop, typeclass} /** @@ -159,18 +158,9 @@ import simulacrum.{noop, typeclass} * }}} * */ - //TODO: Drop from Cats 3.0 @noop def unzip[A, B](fab: F[(A, B)]): (F[A], F[B]) = (map(fab)(_._1), map(fab)(_._2)) - //TODO: Drop `final` when the `noop` version deprecates - // `final` restricts functor instance overriding of `unzip` to the noop version - final def unzip[A, X, Y](fa: F[A])(implicit ev: A <~< (X, Y)): (F[X], F[Y]) = { - val fxy = map(fa)(ev) - - unzip[X, Y](fxy) - } - /** * Lifts `if` to Functor * diff --git a/core/src/main/scala/cats/syntax/functor.scala b/core/src/main/scala/cats/syntax/functor.scala index edf22d762b..958962654d 100644 --- a/core/src/main/scala/cats/syntax/functor.scala +++ b/core/src/main/scala/cats/syntax/functor.scala @@ -1,14 +1,16 @@ package cats package syntax +import cats.evidence.<~< + trait FunctorSyntax extends Functor.ToFunctorOps private[syntax] trait FunctorSyntaxBinCompat0 { - implicit final def catsSyntaxUnzipFunctorOps[F[_], A, B](fa: F[(A, B)]): UnzipFunctorOps[F, A, B] = - new UnzipFunctorOps[F, A, B](fa) + implicit final def catsSyntaxFunctorOps0[F[_], A](fa: F[A]): FunctorOps0[F, A] = + new FunctorOps0[F, A](fa) } -final class UnzipFunctorOps[F[_], A, B](private val fab: F[(A, B)]) extends AnyVal { +final class FunctorOps0[F[_], A](private val fa: F[A]) extends AnyVal { /** * Un-zips an `F[(A, B)]` consisting of element pairs or Tuple2 into two separate F's tupled. @@ -24,5 +26,9 @@ final class UnzipFunctorOps[F[_], A, B](private val fab: F[(A, B)]) extends AnyV * }}} * */ - def unzip(implicit F: Functor[F]): (F[A], F[B]) = (F.map(fab)(_._1), F.map(fab)(_._2)) + def unzip[X, Y](implicit F: Functor[F], ev: A <~< (X, Y)): (F[X], F[Y]) = { + val fxy = F.map(fa)(ev) + + F.unzip(fxy) + } } From ffcaac142dc5f7d2feb10fa640c53f704c84e135 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Wed, 11 Mar 2020 00:39:14 +0800 Subject: [PATCH 07/16] Drop bincompat traits and numerical suffixes, move implicit conversion into FunctorSyntax --- core/src/main/scala/cats/implicits.scala | 1 - core/src/main/scala/cats/syntax/all.scala | 3 --- core/src/main/scala/cats/syntax/functor.scala | 10 ++++------ core/src/main/scala/cats/syntax/package.scala | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/core/src/main/scala/cats/implicits.scala b/core/src/main/scala/cats/implicits.scala index 03b246f075..5c2f3202b9 100644 --- a/core/src/main/scala/cats/implicits.scala +++ b/core/src/main/scala/cats/implicits.scala @@ -9,7 +9,6 @@ object implicits with syntax.AllSyntaxBinCompat4 with syntax.AllSyntaxBinCompat5 with syntax.AllSyntaxBinCompat6 - with syntax.AllSyntaxBinCompat7 with instances.AllInstances with instances.AllInstancesBinCompat0 with instances.AllInstancesBinCompat1 diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index b66c7957f5..c19ea72e45 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -10,7 +10,6 @@ abstract class AllSyntaxBinCompat with AllSyntaxBinCompat4 with AllSyntaxBinCompat5 with AllSyntaxBinCompat6 - with AllSyntaxBinCompat7 trait AllSyntax extends AlternativeSyntax @@ -96,5 +95,3 @@ trait AllSyntaxBinCompat4 trait AllSyntaxBinCompat5 extends ParallelBitraverseSyntax trait AllSyntaxBinCompat6 extends ParallelUnorderedTraverseSyntax - -trait AllSyntaxBinCompat7 extends FunctorSyntaxBinCompat0 diff --git a/core/src/main/scala/cats/syntax/functor.scala b/core/src/main/scala/cats/syntax/functor.scala index 958962654d..ae2029f4c1 100644 --- a/core/src/main/scala/cats/syntax/functor.scala +++ b/core/src/main/scala/cats/syntax/functor.scala @@ -3,14 +3,12 @@ package syntax import cats.evidence.<~< -trait FunctorSyntax extends Functor.ToFunctorOps - -private[syntax] trait FunctorSyntaxBinCompat0 { - implicit final def catsSyntaxFunctorOps0[F[_], A](fa: F[A]): FunctorOps0[F, A] = - new FunctorOps0[F, A](fa) +trait FunctorSyntax extends Functor.ToFunctorOps { + implicit final def catsSyntaxFunctorOps[F[_], A](fa: F[A]): FunctorOps[F, A] = + new FunctorOps[F, A](fa) } -final class FunctorOps0[F[_], A](private val fa: F[A]) extends AnyVal { +final class FunctorOps[F[_], A](private val fa: F[A]) extends AnyVal { /** * Un-zips an `F[(A, B)]` consisting of element pairs or Tuple2 into two separate F's tupled. diff --git a/core/src/main/scala/cats/syntax/package.scala b/core/src/main/scala/cats/syntax/package.scala index ddca7fcac2..022b6a5f5c 100644 --- a/core/src/main/scala/cats/syntax/package.scala +++ b/core/src/main/scala/cats/syntax/package.scala @@ -28,7 +28,7 @@ package object syntax { object eq extends EqSyntax object flatMap extends FlatMapSyntax with FlatMapOptionSyntax object foldable extends FoldableSyntax with FoldableSyntaxBinCompat0 with FoldableSyntaxBinCompat1 - object functor extends FunctorSyntax with FunctorSyntaxBinCompat0 + object functor extends FunctorSyntax object functorFilter extends FunctorFilterSyntax object group extends GroupSyntax object hash extends HashSyntax From aff4a5169c1ab68e7ed58128901e3de8b97858a4 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Wed, 11 Mar 2020 00:40:02 +0800 Subject: [PATCH 08/16] Refactored unzip --- core/src/main/scala/cats/syntax/functor.scala | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/src/main/scala/cats/syntax/functor.scala b/core/src/main/scala/cats/syntax/functor.scala index ae2029f4c1..5ad6098bf9 100644 --- a/core/src/main/scala/cats/syntax/functor.scala +++ b/core/src/main/scala/cats/syntax/functor.scala @@ -24,9 +24,5 @@ final class FunctorOps[F[_], A](private val fa: F[A]) extends AnyVal { * }}} * */ - def unzip[X, Y](implicit F: Functor[F], ev: A <~< (X, Y)): (F[X], F[Y]) = { - val fxy = F.map(fa)(ev) - - F.unzip(fxy) - } + def unzip[X, Y](implicit F: Functor[F], ev: A <~< (X, Y)): (F[X], F[Y]) = F.unzip(F.map(fa)(ev)) } From 052ee9c25a69306b2de4a9228443b2ea506d53ac Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Thu, 10 Sep 2020 00:23:58 +0800 Subject: [PATCH 09/16] Fix formatting --- core/src/main/scala/cats/syntax/functor.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/scala/cats/syntax/functor.scala b/core/src/main/scala/cats/syntax/functor.scala index 5ad6098bf9..3cf9071d45 100644 --- a/core/src/main/scala/cats/syntax/functor.scala +++ b/core/src/main/scala/cats/syntax/functor.scala @@ -22,7 +22,6 @@ final class FunctorOps[F[_], A](private val fa: F[A]) extends AnyVal { * scala> (5: Id[Int]).map(i => (i, i)).unzip == ((5, 5)) * res0: Boolean = true * }}} - * */ def unzip[X, Y](implicit F: Functor[F], ev: A <~< (X, Y)): (F[X], F[Y]) = F.unzip(F.map(fa)(ev)) } From 059ec70626c1f2e5b9fbb323fe85e5d9a390d7c9 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Thu, 10 Sep 2020 01:21:37 +0800 Subject: [PATCH 10/16] Add missing assertions and rewrite unzip compilation test for munit --- .../test/scala/cats/tests/FunctorSuite.scala | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/tests/src/test/scala/cats/tests/FunctorSuite.scala b/tests/src/test/scala/cats/tests/FunctorSuite.scala index 581bc7527e..7dfc4f4f89 100644 --- a/tests/src/test/scala/cats/tests/FunctorSuite.scala +++ b/tests/src/test/scala/cats/tests/FunctorSuite.scala @@ -40,29 +40,40 @@ class FunctorSuite extends CatsSuite { val l = nel.toList val m = nem.toSortedMap - Functor[List].unzip(l.map(i => (i, i))) === ((l, l)) - Functor[Option].unzip(o.map(i => (i, i))) === ((o, o)) - Functor[Map[String, *]].unzip(m.map { case (k, v) => (k, (v, v)) }) === ((m, m)) + assert(Functor[List].unzip(l.map(i => (i, i))) === ((l, l))) + assert(Functor[Option].unzip(o.map(i => (i, i))) === ((o, o))) + assert(Functor[Map[String, *]].unzip(m.map { case (k, v) => (k, (v, v)) }) === ((m, m))) //postfix test for Cats datatypes - nel.map(i => (i, i)).unzip === ((nel, nel)) - nem.map(v => (v, v)).unzip === ((nem, nem)) + assert(nel.map(i => (i, i)).unzip === ((nel, nel))) + assert(nem.map(v => (v, v)).unzip === ((nem, nem))) } //empty test for completeness val emptyL = List.empty[Int] val emptyM = Map.empty[String, Int] - Functor[List].unzip(List.empty[(Int, Int)]) === ((emptyL, emptyL)) - Functor[Map[String, *]].unzip(Map.empty[String, (Int, Int)]) === ((emptyM, emptyM)) + assert(Functor[List].unzip(List.empty[(Int, Int)]) === ((emptyL, emptyL))) + assert(Functor[Map[String, *]].unzip(Map.empty[String, (Int, Int)]) === ((emptyM, emptyM))) } - //TODO: rewrite test with munit -// test("unzip only typechecks for Tuple2") { -// "(NonEmptyList one 1).unzip" mustNot typeCheck -// "(NonEmptyList one ((1, 2))).unzip" must compile -// "(NonEmptyList one ((1, 2, 3))).unzip" mustNot typeCheck -// } + test("unzip only compiles for Tuple2") { + assertNoDiff( + compileErrors("(NonEmptyList one 1).unzip"), + """error: could not find implicit value for parameter ev: Int <~< (X, Y) + |(NonEmptyList one 1).unzip + | ^ + |""".stripMargin + ) + assertNoDiff(compileErrors("(NonEmptyList one ((1, 2))).unzip"), "") + assertNoDiff( + compileErrors("(NonEmptyList one ((1, 2, 3))).unzip"), + """error: could not find implicit value for parameter ev: (Int, Int, Int) <~< (X, Y) + |(NonEmptyList one ((1, 2, 3))).unzip + | ^ + |""".stripMargin + ) + } test("widen equals map(identity)") { forAll { (i: Int) => From 295d9adeb81293e8afd782f3583e5b72444708fe Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Fri, 11 Sep 2020 04:40:17 +0800 Subject: [PATCH 11/16] Reimplement Functor unzip (together with other helpful methods) as Tuple2 extension methods --- core/src/main/scala/cats/syntax/functor.scala | 49 ++++++++++++++++--- .../test/scala/cats/tests/FunctorSuite.scala | 23 +++------ 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/core/src/main/scala/cats/syntax/functor.scala b/core/src/main/scala/cats/syntax/functor.scala index 3cf9071d45..2980424aa2 100644 --- a/core/src/main/scala/cats/syntax/functor.scala +++ b/core/src/main/scala/cats/syntax/functor.scala @@ -1,14 +1,51 @@ package cats package syntax -import cats.evidence.<~< - trait FunctorSyntax extends Functor.ToFunctorOps { - implicit final def catsSyntaxFunctorOps[F[_], A](fa: F[A]): FunctorOps[F, A] = - new FunctorOps[F, A](fa) + implicit final def catsSyntaxFunctorTuple2Ops[F[_], A, B](fab: F[(A, B)]): FunctorTuple2Ops[F, A, B] = + new FunctorTuple2Ops[F, A, B](fab) } -final class FunctorOps[F[_], A](private val fa: F[A]) extends AnyVal { +final class FunctorTuple2Ops[F[_], A, B](private val fab: F[(A, B)]) extends AnyVal { + + /** + * Lifts `Tuple2#_1` to Functor + * + * {{{ + * scala> import cats.Id + * scala> import cats.syntax.functor._ + * + * scala> ((1, 2): Id[(Int, Int)])._1F == 1 + * res0: Boolean = true + * }}} + */ + def _1F(implicit F: Functor[F]): F[A] = F.map(fab)(_._1) + + /** + * Lifts `Tuple2#_2` to Functor + * + * {{{ + * scala> import cats.Id + * scala> import cats.syntax.functor._ + * + * scala> ((1, 2): Id[(Int, Int)])._2F == 2 + * res0: Boolean = true + * }}} + */ + def _2F(implicit F: Functor[F]): F[B] = F.map(fab)(_._2) + + /** + * Lifts `Tuple2#swap` to Functor + * + * {{{ + * scala> import cats.Id + * scala> import cats.syntax.functor._ + * + * scala> ((1, 2): Id[(Int, Int)]).swapF == ((2, 1)) + * res0: Boolean = true + * }}} + */ + def swapF(implicit F: Functor[F]): F[(B, A)] = F.map(fab)(_.swap) /** * Un-zips an `F[(A, B)]` consisting of element pairs or Tuple2 into two separate F's tupled. @@ -23,5 +60,5 @@ final class FunctorOps[F[_], A](private val fa: F[A]) extends AnyVal { * res0: Boolean = true * }}} */ - def unzip[X, Y](implicit F: Functor[F], ev: A <~< (X, Y)): (F[X], F[Y]) = F.unzip(F.map(fa)(ev)) + def unzip(implicit F: Functor[F]): (F[A], F[B]) = F.unzip(fab) } diff --git a/tests/src/test/scala/cats/tests/FunctorSuite.scala b/tests/src/test/scala/cats/tests/FunctorSuite.scala index 7dfc4f4f89..6b4b12a90a 100644 --- a/tests/src/test/scala/cats/tests/FunctorSuite.scala +++ b/tests/src/test/scala/cats/tests/FunctorSuite.scala @@ -57,22 +57,13 @@ class FunctorSuite extends CatsSuite { assert(Functor[Map[String, *]].unzip(Map.empty[String, (Int, Int)]) === ((emptyM, emptyM))) } - test("unzip only compiles for Tuple2") { - assertNoDiff( - compileErrors("(NonEmptyList one 1).unzip"), - """error: could not find implicit value for parameter ev: Int <~< (X, Y) - |(NonEmptyList one 1).unzip - | ^ - |""".stripMargin - ) - assertNoDiff(compileErrors("(NonEmptyList one ((1, 2))).unzip"), "") - assertNoDiff( - compileErrors("(NonEmptyList one ((1, 2, 3))).unzip"), - """error: could not find implicit value for parameter ev: (Int, Int, Int) <~< (X, Y) - |(NonEmptyList one ((1, 2, 3))).unzip - | ^ - |""".stripMargin - ) + test("_1F, _2F and swapF forms correct list for concrete list of tuples") { + forAll { nel: NonEmptyList[(Int, Int)] => + val (nel1, nel2) = nel.unzip + assertEquals(nel._1F, nel1) + assertEquals(nel._2F, nel2) + assertEquals(nel.swapF, nel2.zipWith(nel1)(Tuple2.apply)) + } } test("widen equals map(identity)") { From 2e9d877bc148f9717fc2f6c82e7b7b5429a1d402 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Fri, 11 Sep 2020 05:30:47 +0800 Subject: [PATCH 12/16] Switch NEL to List, not testing unzip here --- tests/src/test/scala/cats/tests/FunctorSuite.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/src/test/scala/cats/tests/FunctorSuite.scala b/tests/src/test/scala/cats/tests/FunctorSuite.scala index 6b4b12a90a..024b2a92e1 100644 --- a/tests/src/test/scala/cats/tests/FunctorSuite.scala +++ b/tests/src/test/scala/cats/tests/FunctorSuite.scala @@ -58,11 +58,11 @@ class FunctorSuite extends CatsSuite { } test("_1F, _2F and swapF forms correct list for concrete list of tuples") { - forAll { nel: NonEmptyList[(Int, Int)] => - val (nel1, nel2) = nel.unzip - assertEquals(nel._1F, nel1) - assertEquals(nel._2F, nel2) - assertEquals(nel.swapF, nel2.zipWith(nel1)(Tuple2.apply)) + forAll { l: List[(Int, Int)] => + val (l1, l2) = l.unzip + assertEquals(l._1F, l1) + assertEquals(l._2F, l2) + assertEquals(l.swapF, l2.zip(l1)) } } From c298b7e9475519dece0bc709b13216629f73f7fb Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Sat, 12 Sep 2020 03:03:36 +0800 Subject: [PATCH 13/16] Switch Id to Option for doctests --- core/src/main/scala/cats/syntax/functor.scala | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/core/src/main/scala/cats/syntax/functor.scala b/core/src/main/scala/cats/syntax/functor.scala index 2980424aa2..b8a9feb638 100644 --- a/core/src/main/scala/cats/syntax/functor.scala +++ b/core/src/main/scala/cats/syntax/functor.scala @@ -12,10 +12,11 @@ final class FunctorTuple2Ops[F[_], A, B](private val fab: F[(A, B)]) extends Any * Lifts `Tuple2#_1` to Functor * * {{{ - * scala> import cats.Id + * scala> import cats.instances.option._ + * scala> import cats.syntax.option._ * scala> import cats.syntax.functor._ * - * scala> ((1, 2): Id[(Int, Int)])._1F == 1 + * scala> (1, 2).some._1F == Some(1) * res0: Boolean = true * }}} */ @@ -25,10 +26,11 @@ final class FunctorTuple2Ops[F[_], A, B](private val fab: F[(A, B)]) extends Any * Lifts `Tuple2#_2` to Functor * * {{{ - * scala> import cats.Id + * scala> import cats.instances.option._ + * scala> import cats.syntax.option._ * scala> import cats.syntax.functor._ * - * scala> ((1, 2): Id[(Int, Int)])._2F == 2 + * scala> (1, 2).some._2F == Some(2) * res0: Boolean = true * }}} */ @@ -38,10 +40,11 @@ final class FunctorTuple2Ops[F[_], A, B](private val fab: F[(A, B)]) extends Any * Lifts `Tuple2#swap` to Functor * * {{{ - * scala> import cats.Id + * scala> import cats.instances.option._ + * scala> import cats.syntax.option._ * scala> import cats.syntax.functor._ * - * scala> ((1, 2): Id[(Int, Int)]).swapF == ((2, 1)) + * scala> (1, 2).some.swapF == Some((2, 1)) * res0: Boolean = true * }}} */ @@ -53,10 +56,11 @@ final class FunctorTuple2Ops[F[_], A, B](private val fab: F[(A, B)]) extends Any * NOTE: Check for effect duplication, possibly memoize before * * {{{ - * scala> import cats.Id + * scala> import cats.instances.option._ + * scala> import cats.syntax.option._ * scala> import cats.syntax.functor._ * - * scala> (5: Id[Int]).map(i => (i, i)).unzip == ((5, 5)) + * scala> 5.some.map(i => (i, i)).unzip == ((Some(5), Some(5))) * res0: Boolean = true * }}} */ From 49ad8b1f32aa17f6fb60a231a14953f4afa91cb1 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Sat, 12 Sep 2020 03:31:22 +0800 Subject: [PATCH 14/16] Fix test name grammar --- tests/src/test/scala/cats/tests/FunctorSuite.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/test/scala/cats/tests/FunctorSuite.scala b/tests/src/test/scala/cats/tests/FunctorSuite.scala index 024b2a92e1..21db844f78 100644 --- a/tests/src/test/scala/cats/tests/FunctorSuite.scala +++ b/tests/src/test/scala/cats/tests/FunctorSuite.scala @@ -57,7 +57,7 @@ class FunctorSuite extends CatsSuite { assert(Functor[Map[String, *]].unzip(Map.empty[String, (Int, Int)]) === ((emptyM, emptyM))) } - test("_1F, _2F and swapF forms correct list for concrete list of tuples") { + test("_1F, _2F and swapF form correct lists for concrete list of tuples") { forAll { l: List[(Int, Int)] => val (l1, l2) = l.unzip assertEquals(l._1F, l1) From 3a6b1f232df658d628ecb23157a6c8658685ada2 Mon Sep 17 00:00:00 2001 From: Ang Hao Yang Date: Sat, 12 Sep 2020 05:23:51 +0800 Subject: [PATCH 15/16] Switch to `Chain` as `Option#unzip` already defined in Scala 2.13 --- core/src/main/scala/cats/syntax/functor.scala | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/core/src/main/scala/cats/syntax/functor.scala b/core/src/main/scala/cats/syntax/functor.scala index b8a9feb638..fe9ff040ff 100644 --- a/core/src/main/scala/cats/syntax/functor.scala +++ b/core/src/main/scala/cats/syntax/functor.scala @@ -12,11 +12,10 @@ final class FunctorTuple2Ops[F[_], A, B](private val fab: F[(A, B)]) extends Any * Lifts `Tuple2#_1` to Functor * * {{{ - * scala> import cats.instances.option._ - * scala> import cats.syntax.option._ + * scala> import cats.data.Chain * scala> import cats.syntax.functor._ * - * scala> (1, 2).some._1F == Some(1) + * scala> Chain((1, 2), (3, 4), (5, 6))._1F == Chain(1, 3, 5) * res0: Boolean = true * }}} */ @@ -26,11 +25,10 @@ final class FunctorTuple2Ops[F[_], A, B](private val fab: F[(A, B)]) extends Any * Lifts `Tuple2#_2` to Functor * * {{{ - * scala> import cats.instances.option._ - * scala> import cats.syntax.option._ + * scala> import cats.data.Chain * scala> import cats.syntax.functor._ * - * scala> (1, 2).some._2F == Some(2) + * scala> Chain((1, 2), (3, 4), (5, 6))._2F == Chain(2, 4, 6) * res0: Boolean = true * }}} */ @@ -40,11 +38,10 @@ final class FunctorTuple2Ops[F[_], A, B](private val fab: F[(A, B)]) extends Any * Lifts `Tuple2#swap` to Functor * * {{{ - * scala> import cats.instances.option._ - * scala> import cats.syntax.option._ + * scala> import cats.data.Chain * scala> import cats.syntax.functor._ * - * scala> (1, 2).some.swapF == Some((2, 1)) + * scala> Chain((1, 2), (3, 4), (5, 6)).swapF == Chain((2, 1), (4, 3), (6, 5)) * res0: Boolean = true * }}} */ @@ -56,11 +53,10 @@ final class FunctorTuple2Ops[F[_], A, B](private val fab: F[(A, B)]) extends Any * NOTE: Check for effect duplication, possibly memoize before * * {{{ - * scala> import cats.instances.option._ - * scala> import cats.syntax.option._ + * scala> import cats.data.Chain * scala> import cats.syntax.functor._ * - * scala> 5.some.map(i => (i, i)).unzip == ((Some(5), Some(5))) + * scala> Chain((1, 2), (3, 4), (5, 6)).unzip == ((Chain(1, 3, 5), Chain(2, 4, 6))) * res0: Boolean = true * }}} */ From 79b6f0640f9ecd9fb6111b8e1ad2796d79d6931e Mon Sep 17 00:00:00 2001 From: Lars Hupel Date: Fri, 5 Feb 2021 22:08:11 +0100 Subject: [PATCH 16/16] Update tests/src/test/scala/cats/tests/FunctorSuite.scala --- tests/src/test/scala/cats/tests/FunctorSuite.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/test/scala/cats/tests/FunctorSuite.scala b/tests/src/test/scala/cats/tests/FunctorSuite.scala index 21db844f78..4c5a123dba 100644 --- a/tests/src/test/scala/cats/tests/FunctorSuite.scala +++ b/tests/src/test/scala/cats/tests/FunctorSuite.scala @@ -58,7 +58,7 @@ class FunctorSuite extends CatsSuite { } test("_1F, _2F and swapF form correct lists for concrete list of tuples") { - forAll { l: List[(Int, Int)] => + forAll { (l: List[(Int, Int)]) => val (l1, l2) = l.unzip assertEquals(l._1F, l1) assertEquals(l._2F, l2)