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

Don't depend on random sampling to determine function equivalence #2577

Merged
merged 7 commits into from
Apr 2, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 17 additions & 15 deletions free/src/test/scala/cats/free/FreeInvariantMonoidalSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package tests

import cats.arrow.FunctionK
import cats.free.FreeInvariantMonoidal
import cats.laws.discipline.{InvariantMonoidalTests, SerializableTests}
import cats.laws.discipline.{InvariantMonoidalTests, MiniInt, SerializableTests}
import cats.laws.discipline.arbitrary._
import cats.laws.discipline.SemigroupalTests.Isomorphisms
import org.scalacheck.{Arbitrary, Gen}
import cats.tests.CsvCodecInvariantMonoidalSuite._
import cats.tests.BinCodecInvariantMonoidalSuite._

class FreeInvariantMonoidalSuite extends CatsSuite {
implicit def freeInvariantMonoidalArbitrary[F[_], A](implicit F: Arbitrary[F[A]],
Expand All @@ -25,24 +26,25 @@ class FreeInvariantMonoidalSuite extends CatsSuite {
}
}

implicit val isoFreeCsvCodec = Isomorphisms.invariant[FreeInvariantMonoidal[CsvCodec, ?]]
implicit val isoFreeBinCodec = Isomorphisms.invariant[FreeInvariantMonoidal[BinCodec, ?]]

checkAll("FreeInvariantMonoidal[CsvCodec, ?]",
InvariantMonoidalTests[FreeInvariantMonoidal[CsvCodec, ?]].invariantMonoidal[Int, Int, Int])
checkAll("InvariantMonoidal[FreeInvariantMonoidal[CsvCodec, ?]]",
SerializableTests.serializable(InvariantMonoidal[FreeInvariantMonoidal[CsvCodec, ?]]))
checkAll("FreeInvariantMonoidal[BinCodec, ?]",
InvariantMonoidalTests[FreeInvariantMonoidal[BinCodec, ?]].invariantMonoidal[MiniInt, Boolean, Boolean])
checkAll("InvariantMonoidal[FreeInvariantMonoidal[BinCodec, ?]]",
SerializableTests.serializable(InvariantMonoidal[FreeInvariantMonoidal[BinCodec, ?]]))

test("FreeInvariantMonoidal#fold") {
val n = 2
val i1 = numericSystemCodec(8)
val i2 = InvariantMonoidal[CsvCodec].point(n)
val iExpr = i1.product(i2.imap(_ * 2)(_ / 2))
forAll { i1: BinCodec[MiniInt] =>
val n = MiniInt.unsafeFromInt(2)
val i2 = InvariantMonoidal[BinCodec].point(n)
val iExpr = i1.product(i2.imap(_ * n)(_ / n))

val f1 = FreeInvariantMonoidal.lift[CsvCodec, Int](i1)
val f2 = FreeInvariantMonoidal.pure[CsvCodec, Int](n)
val fExpr = f1.product(f2.imap(_ * 2)(_ / 2))
val f1 = FreeInvariantMonoidal.lift[BinCodec, MiniInt](i1)
val f2 = FreeInvariantMonoidal.pure[BinCodec, MiniInt](n)
val fExpr = f1.product(f2.imap(_ * n)(_ / n))

fExpr.fold should ===(iExpr)
fExpr.fold should ===(iExpr)
}
}

implicit val idIsInvariantMonoidal: InvariantMonoidal[Id] = new InvariantMonoidal[Id] {
Expand Down
13 changes: 1 addition & 12 deletions free/src/test/scala/cats/free/FreeTSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import cats._
import cats.arrow.FunctionK
import cats.data._
import cats.laws.discipline._
import cats.laws.discipline.arbitrary._
import cats.tests.CatsSuite
import cats.instances.option._
import org.scalacheck.{Arbitrary, Cogen, Gen}
Expand Down Expand Up @@ -247,7 +246,7 @@ trait FreeTSuiteInstances {
import cats.tests.IndexedStateTSuite._
import SemigroupalTests._

type IntState[A] = State[Int, A]
type IntState[A] = State[MiniInt, A]
type FreeTOption[A] = FreeT[Option, Option, A]
type FreeTState[A] = FreeT[IntState, IntState, A]

Expand All @@ -261,16 +260,6 @@ trait FreeTSuiteInstances {
override def map[A, B](fa: JustFunctor[A])(f: A => B): JustFunctor[B] = JustFunctor(f(fa.a))
}

implicit val intEq: Eq[Int] = new Eq[Int] {
def eqv(a: Int, b: Int) = a == b
}

implicit def evalEq[A: Eq]: Eq[Eval[A]] = Eval.catsEqForEval[A]

implicit def intStateEq[A: Eq]: Eq[IntState[A]] = stateEq[Int, A]

implicit def intStateArb[A: Arbitrary]: Arbitrary[IntState[A]] = catsLawArbitraryForState[Int, A]

implicit def freeTOptionEq[A](implicit A: Eq[A], OM: Monad[Option]): Eq[FreeTOption[A]] = new Eq[FreeTOption[A]] {
def eqv(a: FreeTOption[A], b: FreeTOption[A]) = Eq[Option[A]].eqv(a.runM(identity), b.runM(identity))
}
Expand Down
5 changes: 5 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/Arbitrary.scala
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@ object arbitrary extends ArbitraryInstances0 {
implicit def catsLawsCogenForChain[A](implicit A: Cogen[A]): Cogen[Chain[A]] =
Cogen[List[A]].contramap(_.toList)

implicit val catsLawsCogenForMiniInt: Cogen[MiniInt] =
Cogen[Int].contramap(_.toInt)

implicit val catsLawsArbitraryForMiniInt: Arbitrary[MiniInt] =
Arbitrary(Gen.oneOf(MiniInt.allValues))
}

sealed private[discipline] trait ArbitraryInstances0 {
Expand Down
218 changes: 170 additions & 48 deletions laws/src/main/scala/cats/laws/discipline/Eq.scala

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/ExhaustiveCheck.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cats
package laws
package discipline

/**
* An `ExhuastiveCheck[A]` instance can be used similarly to a Scalacheck
* `Gen[A]` instance, but differs in that it generates a `Stream` of the entire
* domain of values as opposed to generating a random sampling of values.
*/
trait ExhaustiveCheck[A] extends Serializable { self =>
def allValues: Stream[A]
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, my thinking is that Stream as a type doesn't enforce exhaustiveness, you probably have a practical case for favoring Stream over List/Chain somewhere right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Eq instances that use ExhaustiveCheck use forall, so my thought was that you wouldn't necessarily need to generate all instances for tests that fail fast. But since this is intended for small domains, there's really no reason to think that Stream would be better than List. I'm happy to change it if you'd like.

}

object ExhaustiveCheck {
def apply[A](implicit A: ExhaustiveCheck[A]): ExhaustiveCheck[A] = A

def instance[A](values: Stream[A]): ExhaustiveCheck[A] = new ExhaustiveCheck[A] {
val allValues: Stream[A] = values
}

implicit val catsLawsExhaustiveCheckForBoolean: ExhaustiveCheck[Boolean] =
instance(Stream(false, true))

implicit val catsLawsExhaustiveCheckForSetBoolean: ExhaustiveCheck[Set[Boolean]] =
forSet[Boolean]

/**
* Warning: the domain of (A, B) is the cross-product of the domain of `A` and the domain of `B`.
*/
implicit def catsLawsExhaustiveCheckForTuple2[A, B](implicit A: ExhaustiveCheck[A],
B: ExhaustiveCheck[B]): ExhaustiveCheck[(A, B)] =
instance(A.allValues.flatMap(a => B.allValues.map(b => (a, b))))

/**
* Warning: the domain of (A, B, C) is the cross-product of the 3 domains.
*/
implicit def catsLawsExhaustiveCheckForTuple3[A, B, C](implicit A: ExhaustiveCheck[A],
B: ExhaustiveCheck[B],
C: ExhaustiveCheck[C]): ExhaustiveCheck[(A, B, C)] =
instance(
for {
a <- A.allValues
b <- B.allValues
c <- C.allValues
} yield (a, b, c)
Copy link
Contributor

Choose a reason for hiding this comment

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

for MiniInt, it will be 349 values, just curious do we actually need this instance or you added it preemptively?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is used by some tests, but I tried to make those places use (MiniInt, Boolean, Boolean) to reduce the domain size a bit. Let me know if you found a place where I failed to do that.

)

implicit def catsLawsExhaustiveCheckForEither[A, B](implicit A: ExhaustiveCheck[A],
B: ExhaustiveCheck[B]): ExhaustiveCheck[Either[A, B]] =
instance(A.allValues.map(Left(_)) ++ B.allValues.map(Right(_)))

implicit def catsLawsExhaustiveCheckForOption[A](implicit A: ExhaustiveCheck[A]): ExhaustiveCheck[Option[A]] =
instance(Stream.cons(None, A.allValues.map(Some(_))))

/**
* Creates an `ExhaustiveCheck[Set[A]]` given an `ExhaustiveCheck[A]` by computing the powerset of
* values. Note that if there are `n` elements in the domain of `A` there will be `2^n` elements
* in the domain of `Set[A]`, so use this only on small domains.
*/
def forSet[A](implicit A: ExhaustiveCheck[A]): ExhaustiveCheck[Set[A]] =
instance(A.allValues.toSet.subsets.toStream)
}
79 changes: 79 additions & 0 deletions laws/src/main/scala/cats/laws/discipline/MiniInt.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package cats
package laws
package discipline

import cats.kernel.{BoundedSemilattice, CommutativeGroup, CommutativeMonoid}
import cats.instances.int._

/**
* Similar to `Int`, but with a much smaller domain. The exact range of [[MiniInt]] may be tuned from time to time, so
* consumers of this type should avoid depending on its exact range.
*
* `MiniInt` has integer overflow characteristics similar to `Int` (but with a smaller radix), meaning that its addition
* and multiplication are commutative and associative.
*/
final class MiniInt private (val intBits: Int) extends AnyVal with Serializable {
import MiniInt._

def unary_- : MiniInt = this * negativeOne

def toInt: Int = intBits << intShift >> intShift

def +(o: MiniInt): MiniInt = wrapped(intBits + o.intBits)
def *(o: MiniInt): MiniInt = wrapped(intBits * o.intBits)
def |(o: MiniInt): MiniInt = wrapped(intBits | o.intBits)
def /(o: MiniInt): MiniInt = wrapped(intBits / o.intBits)

override def toString: String = s"MiniInt(toInt=$toInt, intBits=$intBits)"
}

object MiniInt {
val bitCount: Int = 4
val minIntValue: Int = -8
val maxIntValue: Int = 7
private val intShift: Int = 28
val minValue: MiniInt = unsafeFromInt(minIntValue)
val maxValue: MiniInt = unsafeFromInt(maxIntValue)
val zero: MiniInt = unsafeFromInt(0)
val one: MiniInt = unsafeFromInt(1)
val negativeOne: MiniInt = unsafeFromInt(-1)

def isInDomain(i: Int): Boolean = i >= minIntValue && i <= maxIntValue

def fromInt(i: Int): Option[MiniInt] = if (isInDomain(i)) Some(unsafeFromInt(i)) else None

def wrapped(intBits: Int): MiniInt = new MiniInt(intBits & (-1 >>> intShift))

def unsafeFromInt(i: Int): MiniInt =
if (isInDomain(i)) {
new MiniInt(i << intShift >>> intShift)
} else throw new IllegalArgumentException(s"Expected value between $minIntValue and $maxIntValue but got $i")

val allValues: Stream[MiniInt] = (minIntValue to maxIntValue).map(unsafeFromInt).toStream

implicit val catsLawsEqInstancesForMiniInt: Order[MiniInt] with Hash[MiniInt] = new Order[MiniInt]
with Hash[MiniInt] {
def hash(x: MiniInt): Int = Hash[Int].hash(x.intBits)

def compare(x: MiniInt, y: MiniInt): Int = Order[Int].compare(x.toInt, y.toInt)
}

implicit val catsLawsExhuastiveCheckForMiniInt: ExhaustiveCheck[MiniInt] =
ExhaustiveCheck.instance(allValues)

val miniIntAddition: CommutativeGroup[MiniInt] = new CommutativeGroup[MiniInt] {
val empty = MiniInt.zero
def combine(x: MiniInt, y: MiniInt): MiniInt = x + y
def inverse(x: MiniInt): MiniInt = -x
}

val miniIntMultiplication: CommutativeMonoid[MiniInt] = new CommutativeMonoid[MiniInt] {
val empty = MiniInt.one
def combine(x: MiniInt, y: MiniInt): MiniInt = x * y
}

val miniIntOr: BoundedSemilattice[MiniInt] = new BoundedSemilattice[MiniInt] {
val empty = MiniInt.zero
def combine(x: MiniInt, y: MiniInt): MiniInt = x | y
}
}
Loading