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 18 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.data.Chain
* scala> import cats.syntax.functor._
*
* scala> Chain((1, 2), (3, 4), (5, 6))._1F == Chain(1, 3, 5)
* res0: Boolean = true
* }}}
*/
def _1F(implicit F: Functor[F]): F[A] = F.map(fab)(_._1)

/**
* Lifts `Tuple2#_2` to Functor
*
* {{{
* scala> import cats.data.Chain
* scala> import cats.syntax.functor._
*
* scala> Chain((1, 2), (3, 4), (5, 6))._2F == Chain(2, 4, 6)
* res0: Boolean = true
* }}}
*/
def _2F(implicit F: Functor[F]): F[B] = F.map(fab)(_._2)

/**
* Lifts `Tuple2#swap` to Functor
*
* {{{
* scala> import cats.data.Chain
* scala> import cats.syntax.functor._
*
* scala> Chain((1, 2), (3, 4), (5, 6)).swapF == Chain((2, 1), (4, 3), (6, 5))
* 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.data.Chain
* scala> import cats.syntax.functor._
*
* scala> Chain((1, 2), (3, 4), (5, 6)).unzip == ((Chain(1, 3, 5), Chain(2, 4, 6)))
* 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 form correct lists 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