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

Issue 46: Add laws verification for Read #198

Merged
merged 4 commits into from
Dec 27, 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
37 changes: 32 additions & 5 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ val `scala 2.12` = "2.12.10"
val `scala 2.13` = "2.13.1"

val V = new {
val cats = "2.1.0"
val `cats-discipline` = "1.0.0"
val `discipline-scalatest` = "1.0.0-RC1"
val circe = "0.12.3"
val fs2 = "2.1.0"
val `kind-projector` = "0.11.0"
Expand All @@ -20,6 +23,8 @@ val V = new {
val shapeless = "2.3.3"
}

val `cats-core` = Def.setting("org.typelevel" %% "cats-core" % V.cats)
val `cats-laws` = Def.setting("org.typelevel" %% "cats-laws" % V.cats)
val `circe-core` = Def.setting("io.circe" %%% "circe-core" % V.circe)
val `circe-parser` = Def.setting("io.circe" %%% "circe-parser" % V.circe)
val `fs2-core` = Def.setting("co.fs2" %%% "fs2-core" % V.fs2)
Expand All @@ -32,10 +37,13 @@ val `scodec-core` = Def.setting("org.scodec" %%% "scodec-core" % V.`sco
val `scodec-stream` = Def.setting("org.scodec" %%% "scodec-stream" % V.`scodec-stream`)
val shapeless = Def.setting("com.chuusai" %%% "shapeless" % V.shapeless)

val `circe-generic` = Def.setting("io.circe" %%% "circe-generic" % V.circe % Test)
val `refined-scalacheck` = Def.setting("eu.timepit" %%% "refined-scalacheck" % V.refined % Test)
val scalacheck = Def.setting("org.scalacheck" %%% "scalacheck" % V.scalacheck % Test)
val scalatest = Def.setting("org.scalatest" %%% "scalatest" % V.scalatest % Test)
val `cats-discipline` = Def.setting("org.typelevel" %% "discipline-core" % V.`cats-discipline` % Test)
val `discipline-scalatest` = Def.setting("org.typelevel" %% "discipline-scalatest" % V.`discipline-scalatest` % Test)
val `circe-generic` = Def.setting("io.circe" %%% "circe-generic" % V.circe % Test)
val `refined-scalacheck` = Def.setting("eu.timepit" %%% "refined-scalacheck" % V.refined % Test)
val scalacheck = Def.setting("org.scalacheck" %%% "scalacheck" % V.scalacheck % Test)
val scalatest = Def.setting("org.scalatest" %%% "scalatest" % V.scalatest % Test)

val `scala-parallel-collections` = Def.setting {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, major)) if major >= 13 =>
Expand All @@ -62,6 +70,15 @@ val coreDeps = Def.Initialize.join {
)
}

val coreLawsDeps = Def.Initialize.join {
Seq(
`cats-core`,
`cats-laws`,
`cats-discipline`,
`discipline-scalatest`
)
}

val fs2Deps = Def.Initialize
.join {
Seq(
Expand Down Expand Up @@ -268,6 +285,16 @@ lazy val core = crossProject(JSPlatform, JVMPlatform)
.jsConfigure(_.enablePlugins(BoilerplatePlugin))
.jvmConfigure(_.enablePlugins(BoilerplatePlugin))

lazy val `core-laws` = project
.in(file("core-laws"))
.dependsOn(core.jvm)
.settings(commonSettings ++ testSettings)
.settings(
name := "laserdisc-core-laws",
libraryDependencies ++= coreDeps.value ++ coreLawsDeps.value,
publishArtifact := false
)

lazy val fs2 = project
.in(file("fs2"))
.dependsOn(core.jvm)
Expand Down Expand Up @@ -311,7 +338,7 @@ lazy val circe = crossProject(JSPlatform, JVMPlatform)

lazy val laserdisc = project
.in(file("."))
.aggregate(core.jvm, core.js, fs2, cli, circe.jvm, circe.js)
.aggregate(core.jvm, core.js, `core-laws`, fs2, cli, circe.jvm, circe.js)
.settings(publishSettings)
.settings(
publishArtifact := false,
Expand Down
39 changes: 39 additions & 0 deletions core-laws/src/main/scala/laserdisc/protocol/ReadInstances.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package laserdisc
package protocol

import cats.{Contravariant, Functor, Invariant, Monad}

import scala.util.{Left, Right}

private[protocol] object ReadInstances {
implicit def readFunctor[X]: Functor[X ==> *] =
new Functor[X ==> *] {
override def map[A, B](fa: X ==> A)(f: A => B): X ==> B = fa.map(f)
}

implicit def readContravariant[X]: Contravariant[* ==> X] =
new Contravariant[* ==> X] {
override def contramap[A, B](fa: A ==> X)(f: B => A): B ==> X = fa.contramap(f)
}

implicit def readInvariantFirst[X]: Invariant[* ==> X] =
new Invariant[* ==> X] {
override def imap[A, B](fa: A ==> X)(f: A => B)(g: B => A): B ==> X = fa.contramap(g)
}

implicit def readInvariantSecond[X]: Invariant[X ==> *] =
new Invariant[X ==> *] {
override def imap[A, B](fa: X ==> A)(f: A => B)(g: B => A): X ==> B = fa.map(f)
}

implicit def readMonad[X]: Monad[X ==> *] =
new Monad[X ==> *] {
override def pure[A](x: A): X ==> A = Read.const(x)
override def flatMap[A, B](fa: X ==> A)(f: A => X ==> B): X ==> B = fa.flatMap(f)
override def tailRecM[A, B](a: A)(f: A => X ==> Either[A, B]): X ==> B =
flatMap(f(a)) {
case Left(a) => tailRecM(a)(f)
case Right(b) => pure(b)
}
}
}
92 changes: 92 additions & 0 deletions core-laws/src/test/scala/laserdisc/protocol/ReadLawsCheck.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package laserdisc
package protocol

import cats.instances.int._
import cats.instances.long._
import cats.instances.string._
import cats.laws.discipline.{ContravariantTests, InvariantTests, MonadTests, SerializableTests}
import cats.{Contravariant, Eq, Invariant, Monad}
import org.scalacheck.Gen.chooseNum
import org.scalacheck.{Arbitrary, Cogen, Gen}
import org.scalatest.funsuite.AnyFunSuiteLike
import org.scalatest.matchers.should.Matchers
import org.scalatest.prop.Configuration
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
import org.typelevel.discipline.scalatest.Discipline

final class ReadLawsCheck
extends AnyFunSuiteLike
with Matchers
with ScalaCheckDrivenPropertyChecks
with Discipline
with Configuration
with Implicits {

import ReadInstances._

checkAll("Read[Num, *]", MonadTests[Read[Num, *]].stackUnsafeMonad[Long, String, Long])
checkAll("Monad[Read[Num, *]]", SerializableTests.serializable(Monad[Read[Num, *]]))

checkAll("Read[*, Long]", ContravariantTests[Read[*, Long]].contravariant[Str, Num, Str])
checkAll("Contravariant[Read[*, Long]]", SerializableTests.serializable(Contravariant[Read[*, Long]]))

checkAll("Read[*, Int]", InvariantTests[Read[*, Long]].invariant[Str, Num, Str])
checkAll("Invariant[Read[*, Long]]", SerializableTests.serializable(Invariant[Read[*, Long]]))

checkAll("Read[Num, *]", InvariantTests[Read[Num, *]].invariant[Long, String, Long])
checkAll("Invariant[Read[Num, *]]", SerializableTests.serializable(Invariant[Read[Num, *]]))
}

private[protocol] sealed trait Implicits {
implicit val genNum: Gen[Num] = chooseNum(0L, Long.MaxValue) map Num.apply
implicit val genStr: Gen[Str] = Gen.alphaNumStr map Str.apply

implicit val genListOfNum: Gen[List[Num]] = Gen.listOfN(500, genNum) map (_.distinct)
implicit val genListOfStr: Gen[List[Str]] = Gen.listOfN(500, genStr) map (_.distinct)

implicit def arbNum(implicit ev: Gen[Num]): Arbitrary[Num] = Arbitrary(ev)
implicit def arbStr(implicit ev: Gen[Str]): Arbitrary[Str] = Arbitrary(ev)

implicit val cogenNum: Cogen[Num] = Cogen(_.value)
implicit val cogenStr: Cogen[Str] = Cogen(_.value.length.toLong)

implicit def arbReadNum(implicit ev: Arbitrary[Long]): Arbitrary[Read[Num, Long]] = Arbitrary(ev.arbitrary map Read.const)
implicit def arbReadStr(implicit ev: Arbitrary[Long]): Arbitrary[Read[Str, Long]] = Arbitrary(ev.arbitrary map Read.const)

implicit def arbReadNumString(implicit ev: Arbitrary[Num]): Arbitrary[Read[Num, String]] =
Arbitrary(ev.arbitrary map (n => Read.const(n.value.toString)))

implicit def arbReadNumStringFun(implicit ev: Arbitrary[Num]): Arbitrary[Read[Num, Long => String]] =
Arbitrary(ev.arbitrary map (l => Read.const(_ => l.value.toString)))

implicit def arbReadNumNumFun(implicit ev: Arbitrary[String]): Arbitrary[Read[Num, String => Long]] =
Arbitrary(ev.arbitrary map (s => Read.const(_ => s.length.toLong)))

implicit def eqReadTup[A, B, C, D](implicit ga: Gen[List[A]], eb: Eq[B], ec: Eq[C], ed: Eq[D]): Eq[Read[A, (B, C, D)]] =
new Eq[Read[A, (B, C, D)]] {
override def eqv(x: ==>[A, (B, C, D)], y: ==>[A, (B, C, D)]): Boolean = {
val as = ga.sample.get
as.forall { a =>
(x.read(a), y.read(a)) match {
case (Right((b1, c1, d1)), Right((b2, c2, d2))) => eb.eqv(b1, b2) && ec.eqv(c1, c2) && ed.eqv(d1, d2)
case (Left(e1), Left(e2)) => e1.message == e2.message
case _ => false
}
}
}
}

implicit def eqRead[A, B](implicit ga: Gen[List[A]], eb: Eq[B]): Eq[Read[A, B]] =
new Eq[Read[A, B]] {
override def eqv(x: A ==> B, y: A ==> B): Boolean = {
val as = ga.sample.get
as.forall { a =>
(x.read(a), y.read(a)) match {
case (Right(b1), Right(b2)) => eb.eqv(b1, b2)
case (Left(e1), Left(e2)) => e1.message == e2.message
case _ => false
}
}
}
}
}
6 changes: 4 additions & 2 deletions core/src/main/scala/laserdisc/protocol/Read.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ Note 2: make sure to inspect the combinators as you may be able to leverage some

final def map[C](f: B => C): Read[A, C] = Read.instance(read(_).map(f))

final def flatMap[C](f: B => Read[A, C]): Read[A, C] = Read.instance(a => read(a).flatMap(f(_).read(a)))

final def contramap[C](f: C => A): Read[C, B] = Read.instance(read _ compose f)

final def flatMap[C](f: B => Read[A, C]): Read[A, C] = Read.instance(a => read(a).flatMap(f(_).read(a)))

private[this] final val _extract: Any => Read.Extract[Any] = new Read.Extract[Any](_)
final def unapply(a: A): Read.Extract[RESPDecErr | B] =
_extract(read(a)).asInstanceOf[Read.Extract[RESPDecErr | B]]
Expand All @@ -45,6 +45,8 @@ object Read extends ReadInstances0 {

@inline final def infallible[A, B](f: A => B): Read[A, B] = (a: A) => Right(f(a))

@inline final def const[A, B](b: B): Read[A, B] = Read.infallible(_ => b)

@inline final def instancePF[A, B](expectation: String)(pf: PartialFunction[A, B]): Read[A, B] =
a => if (pf.isDefinedAt(a)) Right(pf(a)) else Left(RESPDecErr(s"Read Error: expected $expectation but was $a"))

Expand Down