Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop noop for Functor unzip with Liskov evidence #3318

Merged
merged 19 commits into from
Feb 6, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion core/src/main/scala/cats/syntax/functor.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,64 @@
package cats
package syntax

trait FunctorSyntax extends Functor.ToFunctorOps
trait FunctorSyntax extends Functor.ToFunctorOps {
implicit final def catsSyntaxFunctorTuple2Ops[F[_], A, B](fab: F[(A, B)]): FunctorTuple2Ops[F, A, B] =
new FunctorTuple2Ops[F, A, B](fab)
}

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
Copy link
Contributor

Choose a reason for hiding this comment

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

I think these docs and test would be nicer with a more interesting Functor than Id. Maybe Option?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek the doc test for unzip was ported from the 2.11 branch so I decided to keep to the same type. Tried Option but it failed for 2.13 because Option#unzip is already defined in 2.13. Switched to Chain as it is Cats specific.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@johnynek is Chain fine or should replace it with something else?

* 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.
*
* 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.unzip(fab)
}
33 changes: 29 additions & 4 deletions tests/src/test/scala/cats/tests/FunctorSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cats.tests

import cats.Functor
import cats.syntax.functor._
import cats.data.{NonEmptyList, NonEmptyMap}
import cats.laws.discipline.arbitrary._
import cats.syntax.eq._
import org.scalacheck.Prop._

Expand Down Expand Up @@ -34,10 +36,33 @@ class FunctorSuite extends CatsSuite {
}

test("unzip preserves structure") {
forAll { (l: List[Int], o: Option[Int], m: Map[String, Int]) =>
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))
forAll { (nel: NonEmptyList[Int], o: Option[Int], nem: NonEmptyMap[String, Int]) =>
val l = nel.toList
val m = nem.toSortedMap

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
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]

assert(Functor[List].unzip(List.empty[(Int, Int)]) === ((emptyL, emptyL)))
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") {
forAll { l: List[(Int, Int)] =>
larsrh marked this conversation as resolved.
Show resolved Hide resolved
val (l1, l2) = l.unzip
assertEquals(l._1F, l1)
assertEquals(l._2F, l2)
assertEquals(l.swapF, l2.zip(l1))
}
}

Expand Down