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

Consider remove dependency on scalacheck from cats-laws #2800

Open
kailuowang opened this issue Apr 19, 2019 · 8 comments
Open

Consider remove dependency on scalacheck from cats-laws #2800

kailuowang opened this issue Apr 19, 2019 · 8 comments

Comments

@kailuowang
Copy link
Contributor

kailuowang commented Apr 19, 2019

Consider abstract out the dependency on scalacheck from cats-laws with some intermediate type class. Then we can have a new module cats-testkit-scalacheck, which is more justfiable to have different versioning which could be more tied to Scalacheck breaking releases. In fact this approach may allow the community to push the dependency on scalacheck to where the tests run, rather than where the law is defined.

Update:
I created a proof of concept for this design in my mind, It compiles to show the type should work
First the usage API on the laws site

class MyRuleSet[P] extends RuleSetDSL[P] {
  //define some laws
  def mylaw[A]= (a:A) => IsEq(a, a)   
  def mylaw2[A, B]= (a:A, b: B) => IsEq(a, a)

  // define the set of laws (potentially inherit from parents
  def ruleSet[A, B](implicit c1 : Check1[A, A],
                             c2: Check2[A, B, A]): Seq[(String, P)] =
    Seq(law("dbad", mylaw[A]),
        law("bgdf", mylaw2[A, B]))
}

In this usage when user added a new law, the compiler will prompt on what CheckX instance is needed.

Here is the law test site that is dependent on scalacheck

class MyRunner extends RunnerBase {
  import cats.kernel.instances.string._
  checkAll(new MyRuleSet[Prop].props[String, String])
}

The Check is a type class that abstract out the operation that converts a defined law A1 => IsEq[A2] to a testable unified type P

trait Checkable1[A1, A2, P] {
  def apply(f: A1 => IsEq[A2]): P
}
trait Checkable2[A1, A2, A3, P] {
  def apply(f: (A1, A2) => IsEq[A3]): P
}

We are going have to create multiple arity version of this type class

The simplified usage on the law site was enabled by the following DSL trait

trait RuleSetDSL[P] {

  type Check1[A1, A2] = Checkable1[A1, A2, P]
  type Check2[A1, A2, A3] = Checkable2[A1, A2, A3, P]

  implicit def law[A1, A2](s : String, f: A1 => IsEq[A2])(implicit c: Check1[A1, A2]): (String, P) =
    (s, c(f))

  implicit def law[A1, A2, A3](s : String, f: (A1, A2) => IsEq[A3])(implicit c: Check2[A1, A2, A3]): (String, P) =
    (s, c(f))
}

apparently we need more arity versions.

On the test site we are going to provide the Checkable Instance for scalacheck derived from Arbitrary instances.

import org.scalacheck.util.Pretty

class RunnerBase {

  //should be trivial to implement to following method with scalatest or claimant
  def checkAll(p: Seq[(String, Prop)]): Unit = ???

  import cats.kernel.Eq
  
  //copied from cats-laws
  implicit def catsLawsIsEqToProp[A](isEq: IsEq[A])(implicit ev: Eq[A], pp: A => Pretty): Prop =
    isEq match {
      case IsEq(x, y) =>
        if (ev.eqv(x, y)) Prop.proved
        else
          Prop.falsified :| {
            val exp = Pretty.pretty[A](y, Pretty.Params(0))
            val act = Pretty.pretty[A](x, Pretty.Params(0))
            s"Expected: $exp\n" + s"Received: $act"
          }
    }

  implicit def tp1[A1: Arbitrary, A2](implicit ev: Eq[A2], pp: A2 => Pretty): Checkable1[A1, A2, Prop] = { f =>
    forAll((a: A1) => f(a))
  }

  implicit def tp2[A1: Arbitrary, A2: Arbitrary, A3](implicit ev: Eq[A3], pp: A3 => Pretty): Checkable2[A1, A2, A3, Prop] = { f =>
    forAll((a:A1, b: A2) => f(a,b))
  }

}

The law definition usage site is quite different with a different set of evidences, but they are probably easier to understand, whatever you law's type is, you just add a corresponding Check[A, B, C] evidence, without the need to understand all the scalacheck types.
What do you guys think?

@travisbrown
Copy link
Contributor

I'm a weak 👎 on this, at least off the top of my head, since ScalaCheck has been around a lot longer than Cats, breaks bincompat very rarely and only for good reasons, and has trustworthy / present maintainers. Also my guess is that plumbing Arbitrary and Shrink instances through this abstraction everywhere will be kind of messy—if someone shows that it can be done nicely I'd be happy to change my mind.

@mpilquist
Copy link
Member

mpilquist commented Apr 19, 2019 via email

@tpolecat
Copy link
Member

Also in favor, if it doesn't complicate things too much. ScalaCheck isn't really state of the art anymore and it would be nice to open things up if we can.

@kailuowang
Copy link
Contributor Author

I added a proof of concept design, any thoughts?

@travisbrown
Copy link
Contributor

@kailuowang Looks reasonable to me (you'll need to add Shrink).

@kailuowang
Copy link
Contributor Author

kailuowang commented Apr 24, 2019

cc @larsrh, any thought?
From my preliminary poc, it seems possible to decouple law testing definition from scalacheck? I introduced an intermediate type class Checkable (strictly speaking a series Checkables with a version for each arity).
Although I think it might force too many changes to the community as all laws tests have to be modified to use Checkable evidence instead of Arbitrarys and Cogens

@larsrh
Copy link
Contributor

larsrh commented Apr 24, 2019

@kailuowang I have no opinion on this.

@tpolecat
Copy link
Member

We do law-checking and I would be fine with mechanical changes. Honestly it's so complicated to get everything set up I can't imagine many people are doing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants