Skip to content

Commit

Permalink
Drop noop for Functor unzip with Liskov evidence (#3318)
Browse files Browse the repository at this point in the history
Co-authored-by: Gagandeep kalra <gagandeepkalra1994@gmail.com>
Co-authored-by: Lars Hupel <lars.hupel@mytum.de>
  • Loading branch information
3 people authored Feb 6, 2021
1 parent 4c9857c commit 1edbbea
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 5 deletions.
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)]) =>
val (l1, l2) = l.unzip
assertEquals(l._1F, l1)
assertEquals(l._2F, l2)
assertEquals(l.swapF, l2.zip(l1))
}
}

Expand Down

0 comments on commit 1edbbea

Please sign in to comment.