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

Make kernel laws consistent with core laws #1922

Merged
merged 21 commits into from
Oct 6, 2017
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
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
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ val binaryCompatibleExceptions = {
exclude[ReversedMissingMethodProblem]("cats.kernel.instances.OptionInstances1.catsKernelStdHashForOption"),
exclude[ReversedMissingMethodProblem]("cats.kernel.instances.FunctionInstances0.catsKernelHashForFunction0"),
exclude[ReversedMissingMethodProblem]("cats.kernel.instances.EitherInstances0.catsStdHashForEither"),
exclude[DirectMissingMethodProblem]("cats.kernel.instances.OptionInstances1.catsKernelStdPartialOrderForOption"),
exclude[DirectMissingMethodProblem]("cats.kernel.instances.all.package.catsKernelStdPartialOrderForBitSet"),
exclude[DirectMissingMethodProblem]("cats.kernel.instances.bitSet.package.catsKernelStdPartialOrderForBitSet"),
//todo: remove these once we release 1.0.0-RC1
Expand Down
10 changes: 4 additions & 6 deletions docs/src/main/tut/typeclasses/lawtesting.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,8 @@ And voila, you've successfully proven that your data type upholds the Functor la
### Testing cats.kernel instances

For most of the type classes included in cats, the above will work great.
However, the law tests for the type classes inside `cats.kernel` are structured a bit differently.
These include `Semigroup`, `Monoid`, `Group` and `Semilattice`.
Instead of importing the laws from `cats.laws.discipline.*`, we have to import `cats.kernel.laws.GroupLaws`
and then call the corresponding method, e.g. `GroupLaws[Foo].monoid`, or `GroupLaws[Foo].semigroup`.
However, the law tests for the type classes inside `cats.kernel` are located in `cats.kernel.laws.discipline.*` instead.
So we have to import from there to test type classes like `Semigroup`, `Monoid`, `Group` or `Semilattice`.

Let's test it out, by defining a `Semigroup` instance for our `Tree` type.

Expand All @@ -135,10 +133,10 @@ Then we can again test the instance inside our class extending `CatsSuite`:

```tut:book
import cats.laws.discipline.FunctorTests
import cats.kernel.laws.GroupLaws
import cats.kernel.laws.discipline.SemigroupLawTests

class TreeLawTests extends CatsSuite {
checkAll("Tree[Int].MonoidLaws", GroupLaws[Tree[Int]].semigroup)
checkAll("Tree[Int].SemigroupLaws", SemigroupLawTests[Tree[Int]].semigroup)
checkAll("Tree.FunctorLaws", FunctorTests[Tree].functor[Int, Int, String])
}
```
6 changes: 3 additions & 3 deletions js/src/test/scala/cats/tests/FutureTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cats
package js
package tests

import cats.kernel.laws.GroupLaws
import cats.kernel.laws.discipline.{MonoidLawTests, SemigroupLawTests}
import cats.laws.discipline._
import cats.js.instances.Await
import cats.js.instances.future.futureComonad
Expand Down Expand Up @@ -58,8 +58,8 @@ class FutureTests extends CatsSuite {

{
implicit val F = ListWrapper.semigroup[Int]
checkAll("Future[ListWrapper[Int]]", GroupLaws[Future[ListWrapper[Int]]].semigroup)
checkAll("Future[ListWrapper[Int]]", SemigroupLawTests[Future[ListWrapper[Int]]].semigroup)
}

checkAll("Future[Int]", GroupLaws[Future[Int]].monoid)
checkAll("Future[Int]", MonoidLawTests[Future[Int]].monoid)
}
6 changes: 3 additions & 3 deletions jvm/src/test/scala/cats/tests/FutureTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cats
package jvm
package tests

import cats.kernel.laws.GroupLaws
import cats.kernel.laws.discipline.{MonoidLawTests, SemigroupLawTests}
import cats.laws.discipline._
import cats.laws.discipline.arbitrary._
import cats.tests.{CatsSuite, ListWrapper}
Expand Down Expand Up @@ -44,8 +44,8 @@ class FutureTests extends CatsSuite {

{
implicit val F = ListWrapper.semigroup[Int]
checkAll("Future[ListWrapper[Int]]", GroupLaws[Future[ListWrapper[Int]]].semigroup)
checkAll("Future[ListWrapper[Int]]", SemigroupLawTests[Future[ListWrapper[Int]]].semigroup)
}

checkAll("Future[Int]", GroupLaws[Future[Int]].monoid)
checkAll("Future[Int]", MonoidLawTests[Future[Int]].monoid)
}
16 changes: 16 additions & 0 deletions kernel-laws/src/main/scala/cats/kernel/laws/BandLaws.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package cats.kernel.laws

import cats.kernel.Band

trait BandLaws[A] extends SemigroupLaws[A] {
override implicit def S: Band[A]

def idempotence(x: A): IsEq[A] =
S.combine(x, x) <-> x

}

object BandLaws {
def apply[A](implicit ev: Band[A]): BandLaws[A] =
new BandLaws[A] { def S: Band[A] = ev }
}
27 changes: 0 additions & 27 deletions kernel-laws/src/main/scala/cats/kernel/laws/BaseLaws.scala

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cats.kernel.laws

import cats.kernel.BoundedSemilattice

trait BoundedSemilatticeLaws[A] extends CommutativeMonoidLaws[A] with SemilatticeLaws[A] {
override implicit def S: BoundedSemilattice[A]

}

object BoundedSemilatticeLaws {
def apply[A](implicit ev: BoundedSemilattice[A]): BoundedSemilatticeLaws[A] =
new BoundedSemilatticeLaws[A] { def S: BoundedSemilattice[A] = ev }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cats
package kernel
package laws


trait CommutativeGroupLaws[A] extends GroupLaws[A] with CommutativeMonoidLaws[A] {
override implicit def S: CommutativeGroup[A]
}

object CommutativeGroupLaws {
def apply[A](implicit ev: CommutativeGroup[A]): CommutativeGroupLaws[A] =
new CommutativeGroupLaws[A] { def S: CommutativeGroup[A] = ev }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cats.kernel.laws

import cats.kernel.CommutativeMonoid

trait CommutativeMonoidLaws[A] extends MonoidLaws[A] with CommutativeSemigroupLaws[A] {
override implicit def S: CommutativeMonoid[A]

}

object CommutativeMonoidLaws {
def apply[A](implicit ev: CommutativeMonoid[A]): CommutativeMonoidLaws[A] =
new CommutativeMonoidLaws[A] { def S: CommutativeMonoid[A] = ev }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package cats.kernel.laws

import cats.kernel.CommutativeSemigroup

trait CommutativeSemigroupLaws[A] extends SemigroupLaws[A] {
override implicit def S: CommutativeSemigroup[A]

def commutative(x: A, y: A): IsEq[A] =
S.combine(x, y) <-> S.combine(y, x)

}

object CommutativeSemigroupLaws {
def apply[A](implicit ev: CommutativeSemigroup[A]): CommutativeSemigroupLaws[A] =
new CommutativeSemigroupLaws[A] { def S: CommutativeSemigroup[A] = ev }
}
26 changes: 26 additions & 0 deletions kernel-laws/src/main/scala/cats/kernel/laws/EqLaws.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package cats.kernel.laws

import cats.kernel.Eq

trait EqLaws[A] {

implicit def E: Eq[A]

def reflexitivityEq(x: A): IsEq[A] =
x <-> x

def symmetryEq(x: A, y: A): IsEq[Boolean] =
E.eqv(x, y) <-> E.eqv(y, x)

def antiSymmetryEq(x: A, y: A, f: A => A): IsEq[Boolean] =
(!E.eqv(x, y) || E.eqv(f(x), f(y))) <-> true

def transitivityEq(x: A, y: A, z: A): IsEq[Boolean] =
(!(E.eqv(x, y) && E.eqv(y, z)) || E.eqv(x, z)) <-> true

}

object EqLaws {
def apply[A](implicit ev: Eq[A]): EqLaws[A] =
new EqLaws[A] { def E: Eq[A] = ev }
}
104 changes: 13 additions & 91 deletions kernel-laws/src/main/scala/cats/kernel/laws/GroupLaws.scala
Original file line number Diff line number Diff line change
@@ -1,101 +1,23 @@
package cats.kernel
package laws

import cats.kernel._
import cats.kernel.instances.option._

import org.typelevel.discipline.Laws
trait GroupLaws[A] extends MonoidLaws[A] {
override implicit def S: Group[A]

import org.scalacheck.{Arbitrary, Prop}
import org.scalacheck.Prop._
def leftInverse(x: A): IsEq[A] =
S.empty <-> S.combine(S.inverse(x), x)

object GroupLaws {
def apply[A : Eq : Arbitrary]: GroupLaws[A] = new GroupLaws[A] {
def Equ = Eq[A]
def Arb = implicitly[Arbitrary[A]]
}
}

trait GroupLaws[A] extends Laws {

implicit def Equ: Eq[A]
implicit def Arb: Arbitrary[A]

// groups

def semigroup(implicit A: Semigroup[A]): GroupProperties = new GroupProperties(
name = "semigroup",
parents = Nil,
Rules.serializable(A),
Rules.associativity(A.combine),
Rules.repeat1("combineN")(A.combineN),
Rules.repeat2("combineN", "|+|")(A.combineN)(A.combine),
"combineAllOption" -> forAll { (xs: Vector[A]) =>
A.combineAllOption(xs) ?== xs.reduceOption(A.combine)
}
)

def band(implicit A: Band[A]): GroupProperties = new GroupProperties(
name = "band",
parents = List(semigroup),
Rules.idempotence(A.combine),
"isIdempotent" -> Semigroup.isIdempotent[A]
)

def commutativeSemigroup(implicit A: CommutativeSemigroup[A]): GroupProperties = new GroupProperties(
name = "commutative semigroup",
parents = List(semigroup),
Rules.commutative(A.combine)
)
def rightInverse(x: A): IsEq[A] =
S.empty <-> S.combine(x, S.inverse(x))

def semilattice(implicit A: Semilattice[A]): GroupProperties = new GroupProperties(
name = "semilattice",
parents = List(band, commutativeSemigroup)
)

def monoid(implicit A: Monoid[A]): GroupProperties = new GroupProperties(
name = "monoid",
parents = List(semigroup),
Rules.leftIdentity(A.empty)(A.combine),
Rules.rightIdentity(A.empty)(A.combine),
Rules.repeat0("combineN", "id", A.empty)(A.combineN),
Rules.collect0("combineAll", "id", A.empty)(A.combineAll),
Rules.isId("isEmpty", A.empty)(A.isEmpty),
"combineAll" -> forAll { (xs: Vector[A]) =>
A.combineAll(xs) ?== (A.empty +: xs).reduce(A.combine)
}
)

def commutativeMonoid(implicit A: CommutativeMonoid[A]): GroupProperties = new GroupProperties(
name = "commutative monoid",
parents = List(monoid, commutativeSemigroup)
)

def boundedSemilattice(implicit A: BoundedSemilattice[A]): GroupProperties = new GroupProperties(
name = "boundedSemilattice",
parents = List(commutativeMonoid, semilattice)
)

def group(implicit A: Group[A]): GroupProperties = new GroupProperties(
name = "group",
parents = List(monoid),
Rules.leftInverse(A.empty)(A.combine)(A.inverse),
Rules.rightInverse(A.empty)(A.combine)(A.inverse),
Rules.consistentInverse("remove")(A.remove)(A.combine)(A.inverse)
)
def consistentInverse(x: A, y: A): IsEq[A] =
S.remove(x, y) <-> S.combine(x, S.inverse(y))
}

def commutativeGroup(implicit A: CommutativeGroup[A]): GroupProperties = new GroupProperties(
name = "commutative group",
parents = List(group, commutativeMonoid)
)
object GroupLaws {
def apply[A](implicit ev: Group[A]): GroupLaws[A] =
new GroupLaws[A] { def S: Group[A] = ev }
}

// property classes

class GroupProperties(
val name: String,
val parents: Seq[GroupProperties],
val props: (String, Prop)*
) extends RuleSet {
val bases = Nil
}
}
69 changes: 19 additions & 50 deletions kernel-laws/src/main/scala/cats/kernel/laws/HashLaws.scala
Original file line number Diff line number Diff line change
@@ -1,58 +1,27 @@
package cats.kernel
package laws

import org.typelevel.discipline._
import org.scalacheck._
import org.scalacheck.Prop._

import scala.util.hashing._

object HashLaws {
def apply[A : Eq : Arbitrary]: HashLaws[A] =
new HashLaws[A] {
def Equ = implicitly[Eq[A]]
def Arb = implicitly[Arbitrary[A]]
}
}
trait HashLaws[A] extends EqLaws[A] {
override implicit def E: Hash[A]

def hashCompatibility(x: A, y: A): IsEq[Boolean] =
(!E.eqv(x, y) || (Hash.hash(x) == Hash.hash(y))) <-> true


/**
* @author Tongfei Chen
*/
trait HashLaws[A] extends Laws {

implicit def Equ: Eq[A]
implicit def Arb: Arbitrary[A]

def hash(implicit A: Hash[A]): HashProperties = new HashProperties(
name = "hash",
parent = None,
Rules.serializable(Equ),
"compatibility-hash" -> forAll { (x: A, y: A) =>
!(A.eqv(x, y)) ?|| (Hash.hash(x) == Hash.hash(y))
}
)

def sameAsUniversalHash(implicit A: Hash[A]): HashProperties = new HashProperties(
name = "sameAsUniversalHash",
parent = None,
Rules.serializable(Equ),
"same-as-universal-hash" -> forAll { (x: A, y: A) =>
(A.hash(x) == x.hashCode) && (Hash.fromUniversalHashCode[A].hash(x) == x.hashCode()) &&
(A.eqv(x, y) == Hash.fromUniversalHashCode[A].eqv(x, y))
}
)

def sameAsScalaHashing(implicit catsHash: Hash[A], scalaHashing: Hashing[A]): HashProperties = new HashProperties(
name = "sameAsScalaHashing",
parent = None,
Rules.serializable(Equ),
"same-as-scala-hashing" -> forAll { (x: A, y: A) =>
(catsHash.hash(x) == Hash.fromHashing(scalaHashing).hash(x)) &&
(catsHash.eqv(x, y) == Hash.fromHashing(scalaHashing).eqv(x, y))
}
)

class HashProperties(name: String, parent: Option[RuleSet], props: (String, Prop)*)
extends DefaultRuleSet(name, parent, props: _*)
def sameAsUniversalHash (x: A, y: A): IsEq[Boolean] =
((E.hash(x) == x.hashCode) && (Hash.fromUniversalHashCode[A].hash(x) == x.hashCode()) &&
(E.eqv(x, y) == Hash.fromUniversalHashCode[A].eqv(x, y))) <-> true


def sameAsScalaHashing(x: A, y: A, scalaHashing: Hashing[A]): IsEq[Boolean] =
((E.hash(x) == Hash.fromHashing(scalaHashing).hash(x)) &&
(E.eqv(x, y) == Hash.fromHashing(scalaHashing).eqv(x, y))) <-> true

}

object HashLaws {
def apply[A](implicit ev: Hash[A]): HashLaws[A] =
new HashLaws[A] { def E: Hash[A] = ev }
}
Loading