-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Changes from all commits
23c5907
637fbe7
e5dd6a0
0151836
c468414
96d78c2
571a120
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
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] | ||
} | ||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
) | ||
|
||
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) | ||
} |
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 | ||
} | ||
} |
There was a problem hiding this comment.
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 favoringStream
overList
/Chain
somewhere right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
Eq
instances that useExhaustiveCheck
useforall
, 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 thatStream
would be better thanList
. I'm happy to change it if you'd like.