Skip to content

Commit

Permalink
Don't depend on random sampling to determine function equivalence
Browse files Browse the repository at this point in the history
This is a work in progress and there is a bit more work that should
probably be done before merging this. However, I have already put a fair
amount of time into this and wanted to see what people thought about it
before pushing through to do all of the relevant work.

Cats has a lot of instances for function-like types. When we go to check
the laws for these types, we are required to supply an `Eq` instance.
But defining equality for functions is awkward. So far we've been
approaching this by generating a bunch of values and passing them into
both functions and ensuring that we get the same results from both. This
can produce false positives if two functions differ but we just happen
to sample values that produce the same output in both. For some
purposes, it isn't a big deal if we get some occasional false positives,
because over many runs of the tests with different RNG seeds, we should
eventually catch any discrepancies.

But here be dragons. Some tests use the results of these equality checks
on the left side of an implication, so a false positive in the equality
check becomes a false _negative_ (and thus a build failure) in the test result.
See [here](typelevel#1666 (comment)) for further discussion.

This is where my adventure with this PR begins. Cats builds have been
timing out quite a bit recently, so I tried to reduce the number of
random values that we sample when comparing two functions for equality.
While this did speed up the build a little bit, it started leading to a
much higher frequency of build failures due to false negatives in tests.

So I started to rethink how we determine function equivalence. Instead
of relying on nondeterministic behavior for equality, we can only
provide function equality for functions whose domains are small enough
to exhaustively check. If two functions produce the same output for the
entirety of their domain, they are equivalent.

I've introduced an `ExhaustiveCheck[A]` type class that is similar to
`Gen[A]` but produces a `Stream[A]` of the entire domain of `A`. I made
the name somewhat specific to tests as opposed to something like
`Finite[A]`, because types like `Int` have a finite domain but would be
prohibitively expensive to exhaustively check in tests and therefore
shouldn't have an instance for this type class.

I also added some `Eq` instances for function-like types whose domains
have `ExhaustiveCheck` instances. For the sake of compatibility, I
didn't remove the old `Eq` instances, but I've put them in a lower
implicit priority scope, and I've changed the sites that were using them
to use the new instances (well not quite all of them yet -- that's why
this PR isn't quite complete yet).

The main benefits of this change as I see it are:

1. Remove some nondeterministic behavior from the build.
2. Allow for shrinking of the number of values checked to improve build
times without triggering build failures.
3. Allow for future deprecation of some problematic instances that are
exposed through cats-laws but that users should probably not depend on.

The main potential downside that I can think of is that we might be
checking 15 examples where we were checking 50 before, which could be
considered a reduction in test coverage. However, I think that all of
the places where this sort of approach is used are parametric on the
type, so I don't think that it should matter much that the domain for
this type is much smaller.

Let me know what you think. If people like this approach then I can
switch over the remaining bits.
  • Loading branch information
ceedubs committed Oct 21, 2018
1 parent 9069dbb commit 23c5907
Show file tree
Hide file tree
Showing 33 changed files with 572 additions and 430 deletions.
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(_.asInt)

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

sealed private[discipline] trait ArbitraryInstances0 {
Expand Down
146 changes: 98 additions & 48 deletions laws/src/main/scala/cats/laws/discipline/Eq.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,103 @@ import cats.data.AndThen
import cats.instances.boolean._
import cats.instances.int._
import cats.instances.string._
import cats.instances.tuple._
import cats.kernel._
import cats.syntax.eq._
import org.scalacheck.Arbitrary

object eq {
object eq extends DisciplineEqInstances {

implicit def catsLawsEqForFn1Exhaustive[A, B](implicit A: ExhaustiveCheck[A], B: Eq[B]): Eq[A => B] =
Eq.instance((f, g) => A.allValues.forall(a => B.eqv(f(a), g(a))))

implicit def catsLawsEqForFn2Exhaustive[A, B, C](implicit A: ExhaustiveCheck[A],
B: ExhaustiveCheck[B],
C: Eq[C]): Eq[(A, B) => C] =
Eq.by((_: (A, B) => C).tupled)(catsLawsEqForFn1Exhaustive)

implicit def catsLawsEqForAndThenExhaustive[A, B](implicit A: ExhaustiveCheck[A], B: Eq[B]): Eq[AndThen[A, B]] =
Eq.instance(catsLawsEqForFn1Exhaustive[A, B].eqv(_, _))

implicit def catsLawsEqForShowExhaustive[A: ExhaustiveCheck]: Eq[Show[A]] =
Eq.by[Show[A], A => String](showA => a => showA.show(a))

implicit def catsLawsEqForEqExhaustive[A: ExhaustiveCheck]: Eq[Eq[A]] =
Eq.by[Eq[A], (A, A) => Boolean](e => (a1, a2) => e.eqv(a1, a2))

implicit def catsLawsEqForEquivExhaustive[A: ExhaustiveCheck]: Eq[Equiv[A]] =
Eq.by[Equiv[A], (A, A) => Boolean](e => (a1, a2) => e.equiv(a1, a2))

implicit def catsLawsEqForPartialOrderExhaustive[A: ExhaustiveCheck]: Eq[PartialOrder[A]] = {
import cats.instances.option._

Eq.by[PartialOrder[A], (A, A) => Option[Int]](o => (a1, a2) => o.tryCompare(a1, a2))
}

implicit def catsLawsEqForPartialOrderingExhaustive[A: ExhaustiveCheck]: Eq[PartialOrdering[A]] = {
import cats.instances.option._

Eq.by[PartialOrdering[A], (A, A) => Option[Int]](
(o: PartialOrdering[A]) => (a1, a2) => o.tryCompare(a1, a2)
)
}

implicit def catsLawsEqForOrderExhaustive[A: ExhaustiveCheck]: Eq[Order[A]] =
Eq.by[Order[A], (A, A) => Int](o => (a1, a2) => o.compare(a1, a2))

implicit def catsLawsEqForOrderingExhaustive[A: ExhaustiveCheck]: Eq[Ordering[A]] =
Eq.by[Ordering[A], (A, A) => Int](o => (a1, a2) => o.compare(a1, a2))

implicit def catsLawsEqForHashExhaustive[A: ExhaustiveCheck]: Eq[Hash[A]] =
Eq.by[Hash[A], A => Int](h => a => h.hash(a))

implicit def catsLawsEqForSemigroupExhaustive[A: ExhaustiveCheck: Eq]: Eq[Semigroup[A]] =
Eq.by[Semigroup[A], (A, A) => A](s => (a1, a2) => s.combine(a1, a2))

implicit def catsLawsEqForCommutativeSemigroupExhaustive[A: ExhaustiveCheck: Eq]: Eq[CommutativeSemigroup[A]] =
Eq.by[CommutativeSemigroup[A], (A, A) => (A, Boolean)](
s => (x, y) => (s.combine(x, y), s.combine(x, y) === s.combine(y, x))
)

implicit def catsLawsEqForMonoid[A](implicit eqSA: Eq[Semigroup[A]], eqA: Eq[A]): Eq[Monoid[A]] = new Eq[Monoid[A]] {
def eqv(f: Monoid[A], g: Monoid[A]): Boolean =
eqSA.eqv(f, g) && eqA.eqv(f.empty, g.empty)
}

implicit def catsLawsEqForSemilattice[A](implicit eqBA: Eq[Band[A]],
eqCA: Eq[CommutativeSemigroup[A]],
eqA: Eq[A]): Eq[Semilattice[A]] =
Eq.instance((f, g) => eqBA.eqv(f, g) && eqCA.eqv(f, g))

implicit def catsLawsEqForCommutativeMonoid[A](implicit eqSA: Eq[CommutativeSemigroup[A]],
eqMA: Eq[Monoid[A]],
eqA: Eq[A]): Eq[CommutativeMonoid[A]] =
Eq.instance((f, g) => eqSA.eqv(f, g) && eqMA.eqv(f, g))

implicit def catsLawsEqForBoundedSemilattice[A](implicit eqSA: Eq[Semilattice[A]],
eqCA: Eq[CommutativeMonoid[A]],
eqA: Eq[A]): Eq[BoundedSemilattice[A]] =
Eq.instance((f, g) => eqSA.eqv(f, g) && eqCA.eqv(f, g))

implicit def catsLawsEqForCommutativeGroup[A](implicit eqMA: Eq[CommutativeMonoid[A]],
eqGA: Eq[Group[A]],
eqA: Eq[A]): Eq[CommutativeGroup[A]] =
Eq.instance((f, g) => eqMA.eqv(f, g) && eqGA.eqv(f, g))

implicit def catsLawsEqForRepresentableStore[F[_]: Representable, S, A](implicit eqFA: Eq[F[A]],
eqS: Eq[S]): Eq[RepresentableStore[F, S, A]] =
Eq.instance((s1, s2) => eqFA.eqv(s1.fa, s2.fa) && eqS.eqv(s1.index, s2.index))
}

/**
* These instances are questionable and can lead to false positives. For the sake of compatibility,
* they haven't been removed, but they should be considered to be deprecated, and we put them in a
* lower implicit scope priority.
*/
private[discipline] trait DisciplineEqInstances {

/**
* Create an approximation of Eq[A => B] by generating 100 values for A
* Create an approximation of Eq[A => B] by generating random values for A
* and comparing the application of the two functions.
*/
implicit def catsLawsEqForFn1[A, B](implicit A: Arbitrary[A], B: Eq[B]): Eq[A => B] = new Eq[A => B] {
Expand All @@ -32,32 +121,22 @@ object eq {
}
}

/** `Eq[AndThen]` instance, built by piggybacking on [[catsLawsEqForFn1]]. */
implicit def catsLawsEqForAndThen[A, B](implicit A: Arbitrary[A], B: Eq[B]): Eq[AndThen[A, B]] =
Eq.instance(catsLawsEqForFn1[A, B].eqv(_, _))

/**
* Create an approximation of Eq[(A, B) => C] by generating 100 values for A and B
* Create an approximation of Eq[(A, B) => C] by generating random values for A and B
* and comparing the application of the two functions.
*/
implicit def catsLawsEqForFn2[A, B, C](implicit A: Arbitrary[A], B: Arbitrary[B], C: Eq[C]): Eq[(A, B) => C] =
new Eq[(A, B) => C] {
val sampleCnt: Int = if (Platform.isJvm) 50 else 5

def eqv(f: (A, B) => C, g: (A, B) => C): Boolean = {
val samples = List.fill(sampleCnt)((A.arbitrary.sample, B.arbitrary.sample)).collect {
case (Some(a), Some(b)) => (a, b)
case _ => sys.error("Could not generate arbitrary values to compare two functions")
}
samples.forall { case (a, b) => C.eqv(f(a, b), g(a, b)) }
}
}
Eq.by((_: (A, B) => C).tupled)(catsLawsEqForFn1)

/** `Eq[AndThen]` instance, built by piggybacking on [[catsLawsEqForFn1]]. */
implicit def catsLawsEqForAndThen[A, B](implicit A: Arbitrary[A], B: Eq[B]): Eq[AndThen[A, B]] =
Eq.instance(catsLawsEqForFn1[A, B].eqv(_, _))

/** Create an approximation of Eq[Show[A]] by using catsLawsEqForFn1[A, String] */
implicit def catsLawsEqForShow[A: Arbitrary]: Eq[Show[A]] =
Eq.by[Show[A], A => String] { showInstance => (a: A) =>
showInstance.show(a)
}
}(catsLawsEqForFn1)

/**
* Create an approximate Eq instance for some type A, by comparing
Expand Down Expand Up @@ -139,26 +218,6 @@ object eq {
f => Function.tupled((x, y) => f.combine(x, y) === f.combine(f.combine(x, y), y))
)(catsLawsEqForFn1[(A, A), Boolean])

implicit def catsLawsEqForMonoid[A](implicit eqSA: Eq[Semigroup[A]], eqA: Eq[A]): Eq[Monoid[A]] = new Eq[Monoid[A]] {
def eqv(f: Monoid[A], g: Monoid[A]): Boolean =
eqSA.eqv(f, g) && eqA.eqv(f.empty, g.empty)
}

implicit def catsLawsEqForSemilattice[A](implicit eqBA: Eq[Band[A]],
eqCA: Eq[CommutativeSemigroup[A]],
eqA: Eq[A]): Eq[Semilattice[A]] =
Eq.instance((f, g) => eqBA.eqv(f, g) && eqCA.eqv(f, g))

implicit def catsLawsEqForCommutativeMonoid[A](implicit eqSA: Eq[CommutativeSemigroup[A]],
eqMA: Eq[Monoid[A]],
eqA: Eq[A]): Eq[CommutativeMonoid[A]] =
Eq.instance((f, g) => eqSA.eqv(f, g) && eqMA.eqv(f, g))

implicit def catsLawsEqForBoundedSemilattice[A](implicit eqSA: Eq[Semilattice[A]],
eqCA: Eq[CommutativeMonoid[A]],
eqA: Eq[A]): Eq[BoundedSemilattice[A]] =
Eq.instance((f, g) => eqSA.eqv(f, g) && eqCA.eqv(f, g))

implicit def catsLawsEqForGroup[A](implicit arbAA: Arbitrary[(A, A)],
eqMA: Eq[Monoid[A]],
eqA: Eq[A]): Eq[Group[A]] = {
Expand All @@ -181,13 +240,4 @@ object eq {

Eq.instance((f, g) => eqMA.eqv(f, g) && inverseEq.eqv(f, g))
}

implicit def catsLawsEqForCommutativeGroup[A](implicit eqMA: Eq[CommutativeMonoid[A]],
eqGA: Eq[Group[A]],
eqA: Eq[A]): Eq[CommutativeGroup[A]] =
Eq.instance((f, g) => eqMA.eqv(f, g) && eqGA.eqv(f, g))

implicit def catsLawsEqForRepresentableStore[F[_]: Representable, S, A](implicit eqFA: Eq[F[A]],
eqS: Eq[S]): Eq[RepresentableStore[F, S, A]] =
Eq.instance((s1, s2) => eqFA.eqv(s1.fa, s2.fa) && eqS.eqv(s1.index, s2.index))
}
55 changes: 55 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,55 @@
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]

def map[B](f: A => B): ExhaustiveCheck[B] = new ExhaustiveCheck[B] {
def allValues: Stream[B] = self.allValues.map(f)
}
}

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))

/**
* 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)
)

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(_))))
}
36 changes: 36 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,36 @@
package cats
package laws
package discipline

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.
*/
final class MiniInt private (val asInt: Int) extends AnyVal with Serializable

object MiniInt {
val minIntValue: Int = -7
val maxIntValue: Int = 7

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

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

def unsafeApply(i: Int): MiniInt =
if (isInDomain(i)) new MiniInt(i)
else throw new IllegalArgumentException(s"Expected value between $minIntValue and $maxIntValue but got $i")

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

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

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

implicit val catsLawsExhuastiveCheckForMiniInt: ExhaustiveCheck[MiniInt] =
ExhaustiveCheck.instance(allValues)
}
21 changes: 10 additions & 11 deletions tests/src/test/scala/cats/tests/AndThenSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,31 @@ import cats.laws.discipline.eq._
import cats.laws.discipline.arbitrary._

class AndThenSuite extends CatsSuite {
{
implicit val iso = SemigroupalTests.Isomorphisms.invariant[AndThen[Int, ?]]
checkAll("AndThen[Int, Int]", SemigroupalTests[AndThen[Int, ?]].semigroupal[Int, Int, Int])
checkAll("Semigroupal[AndThen[Int, ?]]", SerializableTests.serializable(Semigroupal[AndThen[Int, ?]]))
}
checkAll("AndThen[MiniInt, Int]", SemigroupalTests[AndThen[MiniInt, ?]].semigroupal[Int, Int, Int])
checkAll("Semigroupal[AndThen[Int, ?]]", SerializableTests.serializable(Semigroupal[AndThen[Int, ?]]))

{
implicit val iso = SemigroupalTests.Isomorphisms.invariant[AndThen[?, Int]]
checkAll("AndThen[Int, Int]", ContravariantMonoidalTests[AndThen[?, Int]].contravariantMonoidal[Int, Int, Int])
checkAll("AndThen[?, Int]",
ContravariantMonoidalTests[AndThen[?, Int]].contravariantMonoidal[MiniInt, Boolean, Boolean])
checkAll("ContravariantMonoidal[AndThen[?, Int]]",
SerializableTests.serializable(ContravariantMonoidal[AndThen[?, Int]]))
}

checkAll("AndThen[Int, Int]", MonadTests[AndThen[Int, ?]].monad[Int, Int, Int])
checkAll("AndThen[MiniInt, Int]", MonadTests[AndThen[MiniInt, ?]].monad[Int, Int, Int])
checkAll("Monad[AndThen[Int, ?]]", SerializableTests.serializable(Monad[AndThen[Int, ?]]))

checkAll("AndThen[Int, Int]", CommutativeArrowTests[AndThen].commutativeArrow[Int, Int, Int, Int, Int, Int])
checkAll("AndThen",
CommutativeArrowTests[AndThen].commutativeArrow[MiniInt, Boolean, Boolean, Boolean, Boolean, Boolean])
checkAll("Arrow[AndThen]", SerializableTests.serializable(CommutativeArrow[AndThen]))

checkAll("AndThen[Int, Int]", ChoiceTests[AndThen].choice[Int, Int, Int, Int])
checkAll("AndThen", ChoiceTests[AndThen].choice[MiniInt, Boolean, Int, Int])
checkAll("Choice[AndThen]", SerializableTests.serializable(Choice[AndThen]))

checkAll("AndThen[Int, Int]", ArrowChoiceTests[AndThen].arrowChoice[Int, Int, Int, Int, Int, Int])
checkAll("AndThen", ArrowChoiceTests[AndThen].arrowChoice[MiniInt, Boolean, Boolean, Boolean, Boolean, Boolean])
checkAll("ArrowChoice[AndThen]", SerializableTests.serializable(ArrowChoice[AndThen]))

checkAll("AndThen[Int, Int]", ContravariantTests[AndThen[?, Int]].contravariant[Int, Int, Int])
checkAll("AndThen[?, Int]", ContravariantTests[AndThen[?, Int]].contravariant[MiniInt, Int, Boolean])
checkAll("Contravariant[AndThen[?, Int]]", SerializableTests.serializable(Contravariant[AndThen[?, Int]]))

test("compose a chain of functions with andThen") {
Expand Down
14 changes: 9 additions & 5 deletions tests/src/test/scala/cats/tests/BinestedSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@ class BinestedSuite extends CatsSuite {

{
// Profunctor + Functor + Functor = Profunctor
implicit val instance = ListWrapper.functor
implicit val instance = OptionWrapper.functor
Eq[OptionWrapper[MiniInt] => Option[Int]]
checkAll(
"Binested[Function1, OptionWrapper, Option, ?, ?]",
ProfunctorTests[Binested[Function1, OptionWrapper, Option, ?, ?]]
.profunctor[MiniInt, Int, Int, String, String, String]
)
checkAll(
"Binested[Function1, ListWrapper, Option, ?, ?]",
ProfunctorTests[Binested[Function1, ListWrapper, Option, ?, ?]].profunctor[Int, Int, Int, String, String, String]
"Profunctor[Binested[Function1, OptionWrapper, Option, ?, ?]]",
SerializableTests.serializable(Profunctor[Binested[Function1, OptionWrapper, Option, ?, ?]])
)
checkAll("Profunctor[Binested[Function1, ListWrapper, Option, ?, ?]]",
SerializableTests.serializable(Profunctor[Binested[Function1, ListWrapper, Option, ?, ?]]))
}

{
Expand Down
Loading

0 comments on commit 23c5907

Please sign in to comment.