Skip to content

Commit 2718d5e

Browse files
lemasterokailuowang
authored andcommitted
Strong profunctor laws based on category theory (#2640)
* add Strong Laws based * add reference to Notions of Compuations as monoids in StrongLaws * use new laws in Arrow, CommutativeArrow, ArrowChoice test * add link to Haskell impl, refactor common parts
1 parent 663f654 commit 2718d5e

File tree

5 files changed

+80
-22
lines changed

5 files changed

+80
-22
lines changed

laws/src/main/scala/cats/laws/StrongLaws.scala

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,61 @@ package laws
44
import cats.arrow.Strong
55
import cats.syntax.profunctor._
66
import cats.syntax.strong._
7-
import cats.instances.function._
87

98
/**
109
* Laws that must be obeyed by any `cats.functor.Strong`.
10+
*
11+
* See: [[https://arxiv.org/abs/1406.4823 E. Rivas, M. Jaskelioff Notions of Computation as Monoids, Chapter 7]]
12+
* See: [[http://hackage.haskell.org/package/profunctors/docs/Data-Profunctor-Strong.html Haskell Data.Profunctor.Strong]]
1113
*/
1214
trait StrongLaws[F[_, _]] extends ProfunctorLaws[F] {
1315
implicit override def F: Strong[F]
1416

15-
def strongFirstDistributivity[A0, A1, B1, B2, C](fab: F[A1, B1],
16-
f: A0 => A1,
17-
g: B1 => B2): IsEq[F[(A0, C), (B2, C)]] =
18-
fab.dimap(f)(g).first[C] <-> fab.first[C].dimap(f.first[C])(g.first[C])
17+
private def swapTuple[X, Y]: Tuple2[X, Y] => Tuple2[Y, X] = _.swap
1918

20-
def strongSecondDistributivity[A0, A1, B1, B2, C](fab: F[A1, B1],
21-
f: A0 => A1,
22-
g: B1 => B2): IsEq[F[(C, A0), (C, B2)]] =
23-
fab.dimap(f)(g).second[C] <-> fab.second[C].dimap(f.second[C])(g.second[C])
19+
/** first' == dimap swap swap . second' */
20+
def firstIsSwappedSecond[A, B, C](fab: F[A, B]): IsEq[F[(A, C), (B, C)]] =
21+
fab.first[C] <-> fab.second[C].dimap(swapTuple[A, C])(swapTuple[C, B])
22+
23+
/** second' == dimap swap swap . first' */
24+
def secondIsSwappedFirst[A, B, C](fab: F[A, B]): IsEq[F[(C, A), (C, B)]] =
25+
fab.second[C] <-> fab.first[C].dimap(swapTuple[C, A])(swapTuple[B, C])
26+
27+
/** lmap fst == rmap fst . first' */
28+
def lmapEqualsFirstAndThenRmap[A, B, C](fab: F[A, B]): IsEq[F[(A, C), B]] =
29+
fab.lmap[(A, C)]({ case (a, _) => a }) <-> fab.first[C].rmap[B](_._1)
30+
31+
/** lmap snd == rmap snd . second' */
32+
def lmapEqualsSecondAndThenRmap[A, B, C](fab: F[A, B]): IsEq[F[(C, A), B]] =
33+
fab.lmap[(C, A)]({ case (_, b) => b }) <-> fab.second[C].rmap[B](_._2)
34+
35+
private def mapFirst[X, Y, Z](f: X => Z)(cb: (X, Y)): (Z, Y) = (f(cb._1), cb._2)
36+
private def mapSecond[X, Y, Z](f: Y => Z)(cb: (X, Y)): (X, Z) = (cb._1, f(cb._2))
37+
38+
/** lmap (second f) . first == rmap (second f) . first */
39+
def dinaturalityFirst[A, B, C, D](fab: F[A, B], f: C => D): IsEq[F[(A, C), (B, D)]] =
40+
fab.first[C].rmap(mapSecond(f)) <-> fab.first[D].lmap(mapSecond(f))
41+
42+
/** lmap (first f) . second == rmap (first f) . second */
43+
def dinaturalitySecond[A, B, C, D](fab: F[A, B], f: C => D): IsEq[F[(C, A), (D, B)]] =
44+
fab.second[C].rmap(mapFirst(f)) <-> fab.second[D].lmap(mapFirst(f))
45+
46+
private def assoc[A, B, C]: (((A, B), C)) => (A, (B, C)) = { case ((a, c), d) => (a, (c, d)) }
47+
private def unassoc[A, B, C]: ((A, (B, C))) => ((A, B), C) = { case (a, (c, d)) => ((a, c), d) }
48+
49+
/** first' . first' == dimap assoc unassoc . first' where
50+
* assoc ((a,b),c) = (a,(b,c))
51+
* unassoc (a,(b,c)) = ((a,b),c)
52+
*/
53+
def firstFirstIsDimap[A, B, C, D](fab: F[A, B]): IsEq[F[((A, C), D), ((B, C), D)]] =
54+
fab.first[C].first[D] <-> fab.first[(C, D)].dimap[((A, C), D), ((B, C), D)](assoc)(unassoc)
55+
56+
/** second' . second' == dimap unassoc assoc . second' where
57+
* assoc ((a,b),c) = (a,(b,c))
58+
* unassoc (a,(b,c)) = ((a,b),c)
59+
*/
60+
def secondSecondIsDimap[A, B, C, D](fab: F[A, B]): IsEq[F[(D, (C, A)), (D, (C, B))]] =
61+
fab.second[C].second[D] <-> fab.second[(D, C)].dimap[(D, (C, A)), (D, (C, B))](unassoc)(assoc)
2462
}
2563

2664
object StrongLaws {

laws/src/main/scala/cats/laws/discipline/ArrowChoiceTests.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@ trait ArrowChoiceTests[F[_, _]] extends ArrowTests[F] with ChoiceTests[F] {
3434
EqFACBD: Eq[F[(A, C), (B, D)]],
3535
EqFADCD: Eq[F[(A, D), (C, D)]],
3636
EqFADCG: Eq[F[(A, D), (C, G)]],
37-
EqFAEDE: Eq[F[(A, E), (D, E)]],
37+
EqFDADB: Eq[F[(D, A), (D, B)]],
38+
EqFCADB: Eq[F[(C, A), (D, B)]],
3839
EqFABC: Eq[F[A, (B, C)]],
39-
EqFEAED: Eq[F[(E, A), (E, D)]],
4040
EqFACDBCD: Eq[F[((A, C), D), (B, (C, D))]],
41+
EqFACDBCD2: Eq[F[((A, C), D), ((B, C), D)]],
42+
EqFDCADCB: Eq[F[(D, (C, A)), (D, (C, B))]],
43+
EqFCAB: Eq[F[(C, A), B]],
4144
EqFEitherABD: Eq[F[Either[A, B], D]],
4245
EqFEitherCoABC: Eq[F[A, Either[B, C]]],
4346
REqFEitherACD: Eq[F[Either[A, D], Either[C, D]]],

laws/src/main/scala/cats/laws/discipline/ArrowTests.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@ trait ArrowTests[F[_, _]] extends CategoryTests[F] with StrongTests[F] {
3232
EqFACBD: Eq[F[(A, C), (B, D)]],
3333
EqFADCD: Eq[F[(A, D), (C, D)]],
3434
EqFADCG: Eq[F[(A, D), (C, G)]],
35-
EqFAEDE: Eq[F[(A, E), (D, E)]],
35+
EqFDADB: Eq[F[(D, A), (D, B)]],
36+
EqFCADB: Eq[F[(C, A), (D, B)]],
3637
EqFABC: Eq[F[A, (B, C)]],
37-
EqFEAED: Eq[F[(E, A), (E, D)]],
38-
EqFACDBCD: Eq[F[((A, C), D), (B, (C, D))]]
38+
EqFCAB: Eq[F[(C, A), B]],
39+
EqFACDBCD: Eq[F[((A, C), D), (B, (C, D))]],
40+
EqFACDBCD2: Eq[F[((A, C), D), ((B, C), D)]],
41+
EqFDCADCB: Eq[F[(D, (C, A)), (D, (C, B))]]
3942
): RuleSet =
4043
new RuleSet {
4144
def name: String = "arrow"

laws/src/main/scala/cats/laws/discipline/CommutativeArrowTests.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,12 @@ trait CommutativeArrowTests[F[_, _]] extends ArrowTests[F] {
3333
EqFACBD: Eq[F[(A, C), (B, D)]],
3434
EqFADCD: Eq[F[(A, D), (C, D)]],
3535
EqFADCG: Eq[F[(A, D), (C, G)]],
36-
EqFAEDE: Eq[F[(A, E), (D, E)]],
37-
EqFEAED: Eq[F[(E, A), (E, D)]],
38-
EqFACDBCD: Eq[F[((A, C), D), (B, (C, D))]]
36+
EqFDADB: Eq[F[(D, A), (D, B)]],
37+
EqFCADB: Eq[F[(C, A), (D, B)]],
38+
EqFACDBCD: Eq[F[((A, C), D), (B, (C, D))]],
39+
EqFACDBCD2: Eq[F[((A, C), D), ((B, C), D)]],
40+
EqFDCADCB: Eq[F[(D, (C, A)), (D, (C, B))]],
41+
EqFCAB: Eq[F[(C, A), B]]
3942
): RuleSet =
4043
new DefaultRuleSet(name = "commutative arrow",
4144
parent = Some(arrow[A, B, C, D, E, G]),

laws/src/main/scala/cats/laws/discipline/StrongTests.scala

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ trait StrongTests[F[_, _]] extends ProfunctorTests[F] {
1212
def strong[A: Arbitrary, B: Arbitrary, C: Arbitrary, D: Arbitrary, E: Arbitrary, G: Arbitrary](
1313
implicit
1414
ArbFAB: Arbitrary[F[A, B]],
15-
ArbFBC: Arbitrary[F[B, C]],
1615
ArbFCD: Arbitrary[F[C, D]],
1716
CogenA: Cogen[A],
1817
CogenB: Cogen[B],
@@ -22,14 +21,26 @@ trait StrongTests[F[_, _]] extends ProfunctorTests[F] {
2221
EqFAB: Eq[F[A, B]],
2322
EqFAD: Eq[F[A, D]],
2423
EqFAG: Eq[F[A, G]],
25-
EqFAEDE: Eq[F[(A, E), (D, E)]],
26-
EqFEAED: Eq[F[(E, A), (E, D)]]
24+
EqFACBC: Eq[F[(A, C), (B, C)]],
25+
EqFDADB: Eq[F[(D, A), (D, B)]],
26+
EqFACBD: Eq[F[(A, C), (B, D)]],
27+
EqFCADB: Eq[F[(C, A), (D, B)]],
28+
EqFACB: Eq[F[(A, C), B]],
29+
EqFCAB: Eq[F[(C, A), B]],
30+
EqFACDBCD: Eq[F[((A, C), D), ((B, C), D)]],
31+
EqFDCADCB: Eq[F[(D, (C, A)), (D, (C, B))]]
2732
): RuleSet =
2833
new DefaultRuleSet(
2934
name = "strong",
3035
parent = Some(profunctor[A, B, C, D, E, G]),
31-
"strong first distributivity" -> forAll(laws.strongFirstDistributivity[A, B, C, D, E] _),
32-
"strong second distributivity" -> forAll(laws.strongSecondDistributivity[A, B, C, D, E] _)
36+
"first is swapped second" -> forAll(laws.firstIsSwappedSecond[A, B, C] _),
37+
"second is swapped first" -> forAll(laws.secondIsSwappedFirst[A, B, D] _),
38+
"lmap equals first and then rmap" -> forAll(laws.lmapEqualsFirstAndThenRmap[A, B, C] _),
39+
"lmap equals second and then rmap" -> forAll(laws.lmapEqualsSecondAndThenRmap[A, B, C] _),
40+
"dinaturality of first" -> forAll(laws.dinaturalityFirst[A, B, C, D] _),
41+
"dinaturality of second" -> forAll(laws.dinaturalitySecond[A, B, C, D] _),
42+
"first first is dimap" -> forAll(laws.firstFirstIsDimap[A, B, C, D] _),
43+
"second second is dimap" -> forAll(laws.secondSecondIsDimap[A, B, C, D] _)
3344
)
3445
}
3546

0 commit comments

Comments
 (0)