diff --git a/bench/src/main/scala/cats/bench/FoldBench.scala b/bench/src/main/scala/cats/bench/FoldBench.scala index d6388e868e..79021de2c4 100644 --- a/bench/src/main/scala/cats/bench/FoldBench.scala +++ b/bench/src/main/scala/cats/bench/FoldBench.scala @@ -1,6 +1,6 @@ package cats.bench -import algebra.std.string._ +import cats.std.string._ import cats.data.Const import cats.std.list._ import cats.{Foldable, Traverse} diff --git a/build.sbt b/build.sbt index 3187687ea4..1426adaa85 100644 --- a/build.sbt +++ b/build.sbt @@ -23,7 +23,11 @@ lazy val catsDoctestSettings = Seq( ) ++ doctestSettings lazy val commonSettings = Seq( - scalacOptions ++= commonScalacOptions, + scalacOptions ++= commonScalacOptions ++ + (CrossVersion.partialVersion(scalaVersion.value) match { + case Some((2, 10)) => Seq.empty + case _ => Seq("-Ywarn-value-discard") + }), resolvers ++= Seq( "bintray/non" at "http://dl.bintray.com/non/maven", Resolver.sonatypeRepo("releases"), @@ -31,8 +35,6 @@ lazy val commonSettings = Seq( ), libraryDependencies ++= Seq( "com.github.mpilquist" %%% "simulacrum" % "0.5.0", - "org.spire-math" %%% "algebra" % "0.3.1", - "org.spire-math" %%% "algebra-std" % "0.3.1", "org.typelevel" %%% "machinist" % "0.4.1", compilerPlugin("org.scalamacros" %% "paradise" % "2.1.0-M5" cross CrossVersion.full), compilerPlugin("org.spire-math" %% "kind-projector" % "0.6.3") @@ -62,7 +64,7 @@ lazy val disciplineDependencies = Seq( lazy val docSettings = Seq( autoAPIMappings := true, - unidocProjectFilter in (ScalaUnidoc, unidoc) := inProjects(coreJVM), + unidocProjectFilter in (ScalaUnidoc, unidoc) := inProjects(kernelJVM, coreJVM), site.addMappingsToSiteDir(mappings in (ScalaUnidoc, packageDoc), "api"), site.addMappingsToSiteDir(tut, "_tut"), ghpagesNoJekyll := false, @@ -87,7 +89,7 @@ lazy val docs = project .settings(tutSettings) .settings(tutScalacOptions ~= (_.filterNot(Set("-Ywarn-unused-import", "-Ywarn-dead-code")))) .settings(commonJvmSettings) - .dependsOn(coreJVM) + .dependsOn(kernelJVM, coreJVM) lazy val cats = project.in(file(".")) .settings(moduleName := "root") @@ -100,15 +102,15 @@ lazy val catsJVM = project.in(file(".catsJVM")) .settings(moduleName := "cats") .settings(catsSettings) .settings(commonJvmSettings) - .aggregate(macrosJVM, coreJVM, lawsJVM, testsJVM, jvm, docs, bench) - .dependsOn(macrosJVM, coreJVM, lawsJVM, testsJVM % "test-internal -> test", jvm, bench % "compile-internal;test-internal -> test") + .aggregate(macrosJVM, kernelJVM, coreJVM, lawsJVM, testsJVM, jvm, docs, bench) + .dependsOn(macrosJVM, kernelJVM, coreJVM, lawsJVM, testsJVM % "test-internal -> test", jvm, bench % "compile-internal;test-internal -> test") lazy val catsJS = project.in(file(".catsJS")) .settings(moduleName := "cats") .settings(catsSettings) .settings(commonJsSettings) - .aggregate(macrosJS, coreJS, lawsJS, testsJS, js) - .dependsOn(macrosJS, coreJS, lawsJS, testsJS % "test-internal -> test", js) + .aggregate(macrosJS, kernelJS, coreJS, lawsJS, testsJS, js) + .dependsOn(macrosJS, kernelJS, coreJS, lawsJS, testsJS % "test-internal -> test", js) .enablePlugins(ScalaJSPlugin) @@ -123,8 +125,17 @@ lazy val macrosJVM = macros.jvm lazy val macrosJS = macros.js +lazy val kernel = crossProject.crossType(CrossType.Pure) + .settings(moduleName := "cats-kernel") + .settings(catsSettings:_*) + .jsSettings(commonJsSettings:_*) + .jvmSettings(commonJvmSettings:_*) + +lazy val kernelJVM = kernel.jvm +lazy val kernelJS = kernel.js + lazy val core = crossProject.crossType(CrossType.Pure) - .dependsOn(macros) + .dependsOn(macros, kernel) .settings(moduleName := "cats-core") .settings(catsSettings:_*) .settings( @@ -138,12 +149,11 @@ lazy val coreJVM = core.jvm lazy val coreJS = core.js lazy val laws = crossProject.crossType(CrossType.Pure) - .dependsOn(macros, core) + .dependsOn(macros, kernel, core) .settings(moduleName := "cats-laws") .settings(catsSettings:_*) .settings(disciplineDependencies:_*) .settings(libraryDependencies ++= Seq( - "org.spire-math" %%% "algebra-laws" % "0.3.1", "org.typelevel" %%% "catalysts-platform" % "0.0.2")) .jsSettings(commonJsSettings:_*) .jvmSettings(commonJvmSettings:_*) @@ -152,7 +162,7 @@ lazy val lawsJVM = laws.jvm lazy val lawsJS = laws.js lazy val tests = crossProject.crossType(CrossType.Pure) - .dependsOn(macros, core, laws) + .dependsOn(macros, kernel, core, laws) .settings(moduleName := "cats-tests") .settings(catsSettings:_*) .settings(disciplineDependencies:_*) @@ -168,13 +178,13 @@ lazy val testsJS = tests.js // cats-jvm is JVM-only lazy val jvm = project - .dependsOn(macrosJVM, coreJVM, testsJVM % "test-internal -> test") + .dependsOn(macrosJVM, kernelJVM, coreJVM, testsJVM % "test-internal -> test") .settings(moduleName := "cats-jvm") .settings(catsSettings:_*) .settings(commonJvmSettings:_*) // bench is currently JVM-only -lazy val bench = project.dependsOn(macrosJVM, coreJVM, lawsJVM) +lazy val bench = project.dependsOn(macrosJVM, kernelJVM, coreJVM, lawsJVM) .settings(moduleName := "cats-bench") .settings(catsSettings) .settings(noPublishSettings) @@ -183,7 +193,7 @@ lazy val bench = project.dependsOn(macrosJVM, coreJVM, lawsJVM) // cats-js is JS-only lazy val js = project - .dependsOn(macrosJS, coreJS, testsJS % "test-internal -> test") + .dependsOn(macrosJS, kernelJS, coreJS, testsJS % "test-internal -> test") .settings(moduleName := "cats-js") .settings(catsSettings:_*) .settings(commonJsSettings:_*) @@ -207,11 +217,11 @@ lazy val publishSettings = Seq( ) ++ credentialSettings ++ sharedPublishSettings ++ sharedReleaseProcess // These aliases serialise the build for the benefit of Travis-CI. -addCommandAlias("buildJVM", ";macrosJVM/compile;coreJVM/compile;coreJVM/test;lawsJVM/compile;testsJVM/test;jvm/test;bench/test") +addCommandAlias("buildJVM", ";macrosJVM/compile;kernelJVM/compile;coreJVM/compile;kernelJVM/test;coreJVM/test;lawsJVM/compile;testsJVM/test;jvm/test;bench/test") addCommandAlias("validateJVM", ";scalastyle;buildJVM;makeSite") -addCommandAlias("validateJS", ";macrosJS/compile;coreJS/compile;lawsJS/compile;testsJS/test;js/test") +addCommandAlias("validateJS", ";macrosJS/compile;kernelJS/compile;coreJS/compile;lawsJS/compile;testsJS/test;js/test") addCommandAlias("validate", ";validateJS;validateJVM") @@ -269,7 +279,6 @@ lazy val commonScalacOptions = Seq( "-Yno-adapted-args", "-Ywarn-dead-code", "-Ywarn-numeric-widen", - "-Ywarn-value-discard", "-Xfuture" ) diff --git a/core/src/main/scala/cats/package.scala b/core/src/main/scala/cats/package.scala index 00c575017b..c55006c772 100644 --- a/core/src/main/scala/cats/package.scala +++ b/core/src/main/scala/cats/package.scala @@ -44,18 +44,4 @@ package object cats { def traverse[G[_], A, B](a: A)(f: A => G[B])(implicit G: Applicative[G]): G[B] = f(a) } - - type Eq[A] = algebra.Eq[A] - type PartialOrder[A] = algebra.PartialOrder[A] - type Order[A] = algebra.Order[A] - type Semigroup[A] = algebra.Semigroup[A] - type Monoid[A] = algebra.Monoid[A] - type Group[A] = algebra.Group[A] - - val Eq = algebra.Eq - val PartialOrder = algebra.PartialOrder - val Order = algebra.Order - val Semigroup = algebra.Semigroup - val Monoid = algebra.Monoid - val Group = algebra.Group } diff --git a/core/src/main/scala/cats/std/anyval.scala b/core/src/main/scala/cats/std/anyval.scala index 7964aa18e9..9c11fd2cc6 100644 --- a/core/src/main/scala/cats/std/anyval.scala +++ b/core/src/main/scala/cats/std/anyval.scala @@ -1,9 +1,6 @@ package cats package std -import algebra.CommutativeGroup -import algebra.ring.AdditiveCommutativeGroup - trait AnyValInstances extends IntInstances with ByteInstances @@ -15,33 +12,35 @@ trait AnyValInstances with BooleanInstances with UnitInstances -trait IntInstances extends algebra.std.IntInstances { +trait IntInstances { implicit val intShow: Show[Int] = Show.fromToString[Int] - implicit val intGroup: CommutativeGroup[Int] = - AdditiveCommutativeGroup[Int].additive + implicit val intAlgebra: Group[Int] with Order[Int] = + new Group[Int] with Order[Int] { + def empty: Int = 0 + def inverse(a: Int): Int = -a + def combine(x: Int, y: Int): Int = x+y + def compare(x: Int, y: Int): Int = + if (x < y) -1 else if (x > y) 1 else 0 + } } -trait ByteInstances /* missing algebra type classes */ { +trait ByteInstances { implicit val byteShow: Show[Byte] = Show.fromToString[Byte] - // TODO: replace this minimal algebra with one from the algebra project - implicit val byteAlgebra: CommutativeGroup[Byte] with Order[Byte] = - new CommutativeGroup[Byte] with Order[Byte] { - def combine(x: Byte, y: Byte): Byte = (x + y).toByte - def empty: Byte = 0 - def inverse(x: Byte): Byte = (-x).toByte + implicit val byteOrder: Order[Byte] = + new Order[Byte] { def compare(x: Byte, y: Byte): Int = if (x < y) -1 else if (y < x) 1 else 0 } } -trait CharInstances /* missing algebra type classes */ { +trait CharInstances { implicit val charShow: Show[Char] = Show.fromToString[Char] @@ -53,90 +52,80 @@ trait CharInstances /* missing algebra type classes */ { } } -trait ShortInstances /* missing algebra type classes */ { +trait ShortInstances { implicit val shortShow: Show[Short] = Show.fromToString[Short] - // TODO: replace this minimal algebra with one from the algebra project - implicit val shortAlgebra: CommutativeGroup[Short] with Order[Short] = - new CommutativeGroup[Short] with Order[Short] { - def combine(x: Short, y: Short): Short = (x + y).toShort - def empty: Short = 0 - def inverse(x: Short): Short = (-x).toShort + implicit val shortOrder: Order[Short] = + new Order[Short] { def compare(x: Short, y: Short): Int = if (x < y) -1 else if (y < x) 1 else 0 } } -trait LongInstances /* missing algebra type classes */ { +trait LongInstances { implicit val longShow: Show[Long] = Show.fromToString[Long] - // TODO: replace this minimal algebra with one from the algebra project - implicit val longAlgebra: CommutativeGroup[Long] with Order[Long] = - new CommutativeGroup[Long] with Order[Long] { - def combine(x: Long, y: Long): Long = x + y + implicit val longAlgebra: Group[Long] with Order[Long] = + new Group[Long] with Order[Long] { def empty: Long = 0L - def inverse(x: Long): Long = -x + def inverse(a: Long): Long = -a + def combine(x: Long, y: Long): Long = x+y def compare(x: Long, y: Long): Int = if (x < y) -1 else if (y < x) 1 else 0 } } -trait FloatInstances /* missing algebra type classes */ { +trait FloatInstances { implicit val floatShow: Show[Float] = Show.fromToString[Float] - // TODO: replace this minimal algebra with one from the algebra project - implicit val floatAlgebra: CommutativeGroup[Float] with Order[Float] = - new CommutativeGroup[Float] with Order[Float] { - def combine(x: Float, y: Float): Float = x + y - def empty: Float = 0F - def inverse(x: Float): Float = -x + implicit val floatOrder: Order[Float] = + new Order[Float] { def compare(x: Float, y: Float): Int = java.lang.Float.compare(x, y) } } -trait DoubleInstances /* missing algebra type classes */ { +trait DoubleInstances { implicit val doubleShow: Show[Double] = Show.fromToString[Double] - // TODO: replace this minimal algebra with one from the algebra project - implicit val doubleAlgebra: CommutativeGroup[Double] with Order[Double] = - new CommutativeGroup[Double] with Order[Double] { - def combine(x: Double, y: Double): Double = x + y - def empty: Double = 0D - def inverse(x: Double): Double = -x + implicit val doubleOrder: Order[Double] = + new Order[Double] { def compare(x: Double, y: Double): Int = java.lang.Double.compare(x, y) } } -trait BooleanInstances extends algebra.std.BooleanInstances { +trait BooleanInstances { implicit val booleanShow: Show[Boolean] = Show.fromToString[Boolean] + implicit val booleanOrder: Order[Boolean] = + new Order[Boolean] { + def compare(x: Boolean, y: Boolean): Int = + if (x == y) 0 else if (x) 1 else -1 + } + } -trait UnitInstances /* missing algebra type classes */ { +trait UnitInstances { implicit val unitShow: Show[Unit] = Show.fromToString[Unit] - implicit val unitAlgebra: CommutativeGroup[Unit] with Order[Unit] = - new CommutativeGroup[Unit] with Order[Unit] { - def combine(x: Unit, y: Unit): Unit = () - def empty: Unit = () - def inverse(x: Unit): Unit = () + implicit val unitOrder: Order[Unit] = + new Order[Unit] { def compare(x: Unit, y: Unit): Int = 0 } diff --git a/core/src/main/scala/cats/std/bigInt.scala b/core/src/main/scala/cats/std/bigInt.scala index d075af3826..98c471f832 100644 --- a/core/src/main/scala/cats/std/bigInt.scala +++ b/core/src/main/scala/cats/std/bigInt.scala @@ -1,7 +1,14 @@ package cats package std -trait BigIntInstances extends algebra.std.BigIntInstances { +trait BigIntInstances { + implicit val bigIntAlgebra: BigIntAlgebra = + new BigIntAlgebra + implicit val bigIntShow: Show[BigInt] = Show.fromToString[BigInt] } + +class BigIntAlgebra extends Order[BigInt] { + def compare(x: BigInt, y: BigInt): Int = x compare y +} diff --git a/core/src/main/scala/cats/std/function.scala b/core/src/main/scala/cats/std/function.scala index 8d7c3af2d7..e485c44c35 100644 --- a/core/src/main/scala/cats/std/function.scala +++ b/core/src/main/scala/cats/std/function.scala @@ -1,7 +1,6 @@ package cats package std -import algebra.Eq import cats.arrow.{Arrow, Choice} import cats.data.Xor import cats.functor.Contravariant diff --git a/core/src/main/scala/cats/std/list.scala b/core/src/main/scala/cats/std/list.scala index 69575cf14e..3af0b3c619 100644 --- a/core/src/main/scala/cats/std/list.scala +++ b/core/src/main/scala/cats/std/list.scala @@ -1,9 +1,6 @@ package cats package std -import algebra.Eq -import algebra.std.{ListMonoid, ListOrder} - import cats.data.Streaming import cats.syntax.order._ import cats.syntax.show._ @@ -120,3 +117,42 @@ private[std] sealed trait ListInstances2 { } } } + +class ListOrder[A](implicit ev: Order[A]) extends Order[List[A]] { + def compare(xs: List[A], ys: List[A]): Int = { + @tailrec def loop(xs: List[A], ys: List[A]): Int = + xs match { + case Nil => + if (ys.isEmpty) 0 else -1 + case x :: xs => + ys match { + case Nil => 1 + case y :: ys => + val n = ev.compare(x, y) + if (n != 0) n else loop(xs, ys) + } + } + loop(xs, ys) + } +} + +class ListMonoid[A] extends Monoid[List[A]] { + def empty: List[A] = Nil + def combine(x: List[A], y: List[A]): List[A] = x ::: y + + override def combineN(x: List[A], n: Int): List[A] = { + val buf = ListBuffer.empty[A] + @tailrec def loop(i: Int): List[A] = + if (i <= 0) buf.toList else { + buf ++= x + loop(i - 1) + } + loop(n) + } + + override def combineAll(xs: TraversableOnce[List[A]]): List[A] = { + val buf = ListBuffer.empty[A] + xs.foreach(buf ++= _) + buf.toList + } +} diff --git a/core/src/main/scala/cats/std/map.scala b/core/src/main/scala/cats/std/map.scala index bb6e2b35e0..101f72d028 100644 --- a/core/src/main/scala/cats/std/map.scala +++ b/core/src/main/scala/cats/std/map.scala @@ -1,9 +1,11 @@ package cats package std +import scala.collection.mutable + import cats.syntax.eq._ -trait MapInstances extends algebra.std.MapInstances { +trait MapInstances { implicit def MapEq[A, B: Eq]: Eq[Map[A, B]] = new Eq[Map[A, B]] { @@ -64,4 +66,72 @@ trait MapInstances extends algebra.std.MapInstances { override def isEmpty[A](fa: Map[K, A]): Boolean = fa.isEmpty } + + implicit def mapMonoid[K, V: Semigroup]: MapMonoid[K, V] = + new MapMonoid[K, V] + + implicit def mapGroup[K, V: Group]: MapGroup[K, V] = + new MapGroup[K, V] +} + +class MapMonoid[K, V](implicit V: Semigroup[V]) extends Monoid[Map[K, V]] { + def empty: Map[K, V] = Map.empty + + def combine(x: Map[K, V], y: Map[K, V]): Map[K, V] = + MapMethods.addMap(x, y)(V.combine) +} + +class MapGroup[K, V](implicit V: Group[V]) extends MapMonoid[K, V] with Group[Map[K, V]] { + def inverse(x: Map[K, V]): Map[K, V] = + x.map { case (k, v) => (k, V.inverse(v)) } + + override def remove(x: Map[K, V], y: Map[K, V]): Map[K, V] = + MapMethods.subtractMap(x, y)(V.remove)(V.inverse) +} + +object MapMethods { + def initMutableMap[K, V](m: Map[K, V]): mutable.Map[K, V] = { + val result = mutable.Map.empty[K, V] + m.foreach { case (k, v) => result(k) = v } + result + } + + def wrapMutableMap[K, V](m: mutable.Map[K, V]): Map[K, V] = + new WrappedMutableMap(m) + + private[cats] class WrappedMutableMap[K, V](m: mutable.Map[K, V]) extends Map[K, V] { + override def size: Int = m.size + def get(k: K): Option[V] = m.get(k) + def iterator: Iterator[(K, V)] = m.iterator + def +[V2 >: V](kv: (K, V2)): Map[K, V2] = m.toMap + kv + def -(key: K): Map[K, V] = m.toMap - key + } + + def addMap[K, V](x: Map[K, V], y: Map[K, V])(f: (V, V) => V): Map[K, V] = { + val (small, big, g) = + if (x.size <= y.size) (x, y, f) + else (y, x, (v1: V, v2: V) => f(v2, v1)) + + val m = initMutableMap(big) + small.foreach { case (k, v1) => + m(k) = m.get(k) match { + case Some(v2) => g(v1, v2) + case None => v1 + } + } + wrapMutableMap(m) + } + + def subtractMap[K, V](x: Map[K, V], y: Map[K, V])(subtract: (V, V) => V)(negate: V => V): Map[K, V] = { + // even if x is smaller, we'd need to call map/foreach on y to + // negate all its values, so this is just as fast or faster. + val m = initMutableMap(x) + y.foreach { case (k, v2) => + m(k) = m.get(k) match { + case Some(v1) => subtract(v1, v2) + case None => negate(v2) + } + } + wrapMutableMap(m) + } } diff --git a/core/src/main/scala/cats/std/option.scala b/core/src/main/scala/cats/std/option.scala index 4439a4e0a0..dcaeee56e7 100644 --- a/core/src/main/scala/cats/std/option.scala +++ b/core/src/main/scala/cats/std/option.scala @@ -1,8 +1,6 @@ package cats package std -import algebra.Eq - trait OptionInstances extends OptionInstances1 { implicit val optionInstance: Traverse[Option] with MonadCombine[Option] with CoflatMap[Option] with Alternative[Option] = new Traverse[Option] with MonadCombine[Option] with CoflatMap[Option] with Alternative[Option] { diff --git a/core/src/main/scala/cats/std/set.scala b/core/src/main/scala/cats/std/set.scala index 1efeff8350..4849ef43d4 100644 --- a/core/src/main/scala/cats/std/set.scala +++ b/core/src/main/scala/cats/std/set.scala @@ -3,7 +3,7 @@ package std import cats.syntax.show._ -trait SetInstances extends algebra.std.SetInstances { +trait SetInstances { implicit val setInstance: Foldable[Set] with MonoidK[Set] = new Foldable[Set] with MonoidK[Set] { @@ -28,8 +28,20 @@ trait SetInstances extends algebra.std.SetInstances { implicit def setMonoid[A]: Monoid[Set[A]] = MonoidK[Set].algebra[A] + implicit def setPartialOrder[A]: PartialOrder[Set[A]] = new SetPartialOrder[A] + implicit def setShow[A:Show]: Show[Set[A]] = new Show[Set[A]] { def show(fa: Set[A]): String = fa.toIterator.map(_.show).mkString("Set(", ", ", ")") } } + +class SetPartialOrder[A] extends PartialOrder[Set[A]] { + def partialCompare(x: Set[A], y: Set[A]): Double = + if (x.size < y.size) if (x.subsetOf(y)) -1.0 else Double.NaN + else if (y.size < x.size) -partialCompare(y, x) + else if (x == y) 0.0 + else Double.NaN + + override def eqv(x: Set[A], y: Set[A]): Boolean = x == y +} diff --git a/core/src/main/scala/cats/std/string.scala b/core/src/main/scala/cats/std/string.scala index a79a814734..a7b5cb2200 100644 --- a/core/src/main/scala/cats/std/string.scala +++ b/core/src/main/scala/cats/std/string.scala @@ -1,7 +1,24 @@ package cats package std -trait StringInstances extends algebra.std.StringInstances { +trait StringInstances { + implicit val stringOrder: Order[String] = new StringOrder + implicit val stringMonoid = new StringMonoid implicit val stringShow: Show[String] = Show.fromToString[String] } + +class StringOrder extends Order[String] { + def compare(x: String, y: String): Int = x compare y +} + +class StringMonoid extends Monoid[String] { + def empty: String = "" + def combine(x: String, y: String): String = x + y + + override def combineAll(xs: TraversableOnce[String]): String = { + val sb = new StringBuilder + xs.foreach(sb.append) + sb.toString + } +} diff --git a/kernel/src/main/scala/cats/Eq.scala b/kernel/src/main/scala/cats/Eq.scala new file mode 100644 index 0000000000..2d4b034f5f --- /dev/null +++ b/kernel/src/main/scala/cats/Eq.scala @@ -0,0 +1,106 @@ +package cats + +import scala.{specialized => sp} + +import scala.math.Equiv + +/** + * A type class used to determine equality between 2 instances of the same + * type. Any 2 instances `x` and `y` are equal if `eqv(x, y)` is `true`. + * Moreover, `eqv` should form an equivalence relation. + */ +trait Eq[@sp A] extends Any with Serializable { self => + + /** + * Returns `true` if `x` and `y` are equivalent, `false` otherwise. + */ + def eqv(x: A, y: A): Boolean + + /** + * Returns `false` if `x` and `y` are equivalent, `true` otherwise. + */ + def neqv(x: A, y: A): Boolean = !eqv(x, y) + + /** + * Constructs a new `Eq` instance for type `B` where 2 elements are + * equivalent iff `eqv(f(x), f(y))`. + */ + def on[@sp B](f: B => A): Eq[B] = + new Eq[B] { + def eqv(x: B, y: B): Boolean = self.eqv(f(x), f(y)) + } + + /** + * Return an Eq that gives the result of the and of this and that + * note this is idempotent + */ + def and(that: Eq[A]): Eq[A] = + new Eq[A] { + def eqv(x: A, y: A) = self.eqv(x, y) && that.eqv(x, y) + } + /** + * Return an Eq that gives the result of the or of this and that + * Note this is idempotent + */ + def or(that: Eq[A]): Eq[A] = + new Eq[A] { + def eqv(x: A, y: A) = self.eqv(x, y) || that.eqv(x, y) + } +} + +trait EqFunctions { + def eqv[@sp A](x: A, y: A)(implicit ev: Eq[A]): Boolean = + ev.eqv(x, y) + + def neqv[@sp A](x: A, y: A)(implicit ev: Eq[A]): Boolean = + ev.neqv(x, y) +} + +object Eq extends EqFunctions { + + /** + * Access an implicit `Eq[A]`. + */ + @inline final def apply[A](implicit ev: Eq[A]): Eq[A] = ev + + /** + * Convert an implicit `Eq[B]` to an `Eq[A]` using the given + * function `f`. + */ + def by[@sp A, @sp B](f: A => B)(implicit ev: Eq[B]): Eq[A] = + ev.on(f) + + /** + * This gives compatibility with scala's Equiv trait + */ + implicit def equiv[A](implicit ev: Eq[A]): Equiv[A] = + new Equiv[A] { + def equiv(a: A, b: A) = ev.eqv(a, b) + } + + /** + * Create an `Eq` instance from an `eqv` implementation. + */ + def instance[A](f: (A, A) => Boolean): Eq[A] = + new Eq[A] { + def eqv(x: A, y: A) = f(x, y) + } + + /** + * An `Eq[A]` that delegates to universal equality (`==`). + * + * This can be useful for case classes, which have reasonable `equals` + * implementations + */ + def fromUniversalEquals[A]: Eq[A] = + new Eq[A] { + def eqv(x: A, y: A) = x == y + } + + /** + * Everything is the same + */ + def allEqual[A]: Eq[A] = new Eq[A] { + def eqv(x: A, y: A) = true + } +} diff --git a/kernel/src/main/scala/cats/Group.scala b/kernel/src/main/scala/cats/Group.scala new file mode 100644 index 0000000000..b5db4eed7f --- /dev/null +++ b/kernel/src/main/scala/cats/Group.scala @@ -0,0 +1,48 @@ +package cats + +import scala.{ specialized => sp } + +/** + * A group is a monoid where each element has an inverse. + */ +trait Group[@sp(Int, Long, Float, Double) A] extends Any with Monoid[A] { + + /** + * Find the inverse of `a`. + * + * `combine(a, inverse(a))` = `combine(inverse(a), a)` = `empty`. + */ + def inverse(a: A): A + + /** + * Remove the element `b` from `a`. + * + * Equivalent to `combine(a, inverse(a))` + */ + def remove(a: A, b: A): A = combine(a, inverse(b)) + + /** + * Return `a` appended to itself `n` times. If `n` is negative, then + * this returns `inverse(a)` appended to itself `n` times. + */ + override def combineN(a: A, n: Int): A = + if (n > 0) repeatedCombineN(a, n) + else if (n == 0) empty + else if (n == Int.MinValue) combineN(inverse(combine(a, a)), 1073741824) + else repeatedCombineN(inverse(a), -n) +} + +trait GroupFunctions[G[T] <: Group[T]] extends MonoidFunctions[Group] { + def inverse[@sp(Int, Long, Float, Double) A](a: A)(implicit ev: G[A]): A = + ev.inverse(a) + def remove[@sp(Int, Long, Float, Double) A](x: A, y: A)(implicit ev: G[A]): A = + ev.remove(x, y) +} + +object Group extends GroupFunctions[Group] { + + /** + * Access an implicit `Group[A]`. + */ + @inline final def apply[A](implicit ev: Group[A]): Group[A] = ev +} diff --git a/kernel/src/main/scala/cats/Monoid.scala b/kernel/src/main/scala/cats/Monoid.scala new file mode 100644 index 0000000000..32f9f334b7 --- /dev/null +++ b/kernel/src/main/scala/cats/Monoid.scala @@ -0,0 +1,52 @@ +package cats + +import scala.{ specialized => sp } + +/** + * A monoid is a semigroup with an identity. A monoid is a specialization of a + * semigroup, so its operation must be associative. Additionally, + * `combine(x, empty) == combine(empty, x) == x`. For example, if we have `Monoid[String]`, + * with `combine` as string concatenation, then `empty = ""`. + */ +trait Monoid[@sp(Int, Long, Float, Double) A] extends Any with Semigroup[A] { + + /** + * Return the identity element for this monoid. + */ + def empty: A + + /** + * Tests if `a` is the identity. + */ + def isEmpty(a: A)(implicit ev: Eq[A]) = ev.eqv(a, empty) + + /** + * Return `a` appended to itself `n` times. + */ + override def combineN(a: A, n: Int): A = + if (n < 0) throw new IllegalArgumentException("Repeated combining for monoids must have n >= 0") + else if (n == 0) empty + else repeatedCombineN(a, n) + + /** + * Given a sequence of `as`, sum them using the monoid and return the total. + */ + def combineAll(as: TraversableOnce[A]): A = + as.foldLeft(empty)(combine) +} + +trait MonoidFunctions[M[T] <: Monoid[T]] extends SemigroupFunctions[M] { + def empty[@sp(Int, Long, Float, Double) A](implicit ev: M[A]): A = + ev.empty + + def combineAll[@sp(Int, Long, Float, Double) A](as: TraversableOnce[A])(implicit ev: M[A]): A = + ev.combineAll(as) +} + +object Monoid extends MonoidFunctions[Monoid] { + + /** + * Access an implicit `Monoid[A]`. + */ + @inline final def apply[A](implicit ev: Monoid[A]): Monoid[A] = ev +} diff --git a/kernel/src/main/scala/cats/Order.scala b/kernel/src/main/scala/cats/Order.scala new file mode 100644 index 0000000000..8af47bfb3e --- /dev/null +++ b/kernel/src/main/scala/cats/Order.scala @@ -0,0 +1,209 @@ +package cats + +import scala.{specialized => sp} + +/** + * The `Order` type class is used to define a total ordering on some type `A`. + * An order is defined by a relation <=, which obeys the following laws: + * + * - either x <= y or y <= x (totality) + * - if x <= y and y <= x, then x == y (antisymmetry) + * - if x <= y and y <= z, then x <= z (transitivity) + * + * The truth table for compare is defined as follows: + * + * x <= y x >= y Int + * true true = 0 (corresponds to x == y) + * true false < 0 (corresponds to x < y) + * false true > 0 (corresponds to x > y) + * + * By the totality law, x <= y and y <= x cannot be both false. + */ +trait Order[@sp A] extends Any with PartialOrder[A] { self => + + /** + * Result of comparing `x` with `y`. Returns an Int whose sign is: + * - negative iff `x < y` + * - zero iff `x = y` + * - positive iff `x > y` + */ + def compare(x: A, y: A): Int + + def partialCompare(x: A, y: A): Double = compare(x, y).toDouble + + /** + * If x <= y, return x, else return y. + */ + def min(x: A, y: A): A = if (lt(x, y)) x else y + + /** + * If x >= y, return x, else return y. + */ + def max(x: A, y: A): A = if (gt(x, y)) x else y + + /** + * Defines an order on `B` by mapping `B` to `A` using `f` and using `A`s + * order to order `B`. + */ + override def on[@sp B](f: B => A): Order[B] = + new Order[B] { + def compare(x: B, y: B): Int = self.compare(f(x), f(y)) + } + + /** + * Defines an ordering on `A` where all arrows switch direction. + */ + override def reverse: Order[A] = + new Order[A] { + def compare(x: A, y: A): Int = self.compare(y, x) + + override def reverse: Order[A] = self + } + + // The following may be overridden for performance: + + /** + * Returns true if `x` = `y`, false otherwise. + */ + override def eqv(x: A, y: A): Boolean = + compare(x, y) == 0 + + /** + * Returns true if `x` != `y`, false otherwise. + */ + override def neqv(x: A, y: A): Boolean = + compare(x, y) != 0 + + /** + * Returns true if `x` <= `y`, false otherwise. + */ + override def lteqv(x: A, y: A): Boolean = + compare(x, y) <= 0 + + /** + * Returns true if `x` < `y`, false otherwise. + */ + override def lt(x: A, y: A): Boolean = + compare(x, y) < 0 + + /** + * Returns true if `x` >= `y`, false otherwise. + */ + override def gteqv(x: A, y: A): Boolean = + compare(x, y) >= 0 + + /** + * Returns true if `x` > `y`, false otherwise. + */ + override def gt(x: A, y: A): Boolean = + compare(x, y) > 0 + + /** + * Returns a new `Order[A]` instance that first compares by the original + * `Order` instance and uses the provided `Order` instance to "break ties". + * + * That is, `x.whenEqual(y)` creates an `Order` that first orders by `x` and + * then (if two elements are equal) falls back to `y` for the comparison. + */ + def whenEqual(o: Order[A]): Order[A] = new Order[A] { + def compare(x: A, y: A) = { + val c = self.compare(x, y) + if (c == 0) o.compare(x, y) + else c + } + } + + /** + * Convert a `Order[A]` to a `scala.math.Ordering[A]` + * instance. + */ + def toOrdering: Ordering[A] = new Ordering[A] { + def compare(x: A, y: A): Int = self.compare(x, y) + } +} + +trait OrderFunctions { + def compare[@sp A](x: A, y: A)(implicit ev: Order[A]): Int = + ev.compare(x, y) + + def eqv[@sp A](x: A, y: A)(implicit ev: Order[A]): Boolean = + ev.eqv(x, y) + def neqv[@sp A](x: A, y: A)(implicit ev: Order[A]): Boolean = + ev.neqv(x, y) + def gt[@sp A](x: A, y: A)(implicit ev: Order[A]): Boolean = + ev.gt(x, y) + def gteqv[@sp A](x: A, y: A)(implicit ev: Order[A]): Boolean = + ev.gteqv(x, y) + def lt[@sp A](x: A, y: A)(implicit ev: Order[A]): Boolean = + ev.lt(x, y) + def lteqv[@sp A](x: A, y: A)(implicit ev: Order[A]): Boolean = + ev.lteqv(x, y) + + def min[@sp A](x: A, y: A)(implicit ev: Order[A]): A = + ev.min(x, y) + def max[@sp A](x: A, y: A)(implicit ev: Order[A]): A = + ev.max(x, y) +} + +object Order extends OrderFunctions { + + /** + * Access an implicit `Eq[A]`. + */ + @inline final def apply[A](implicit ev: Order[A]) = ev + + /** + * Convert an implicit `Order[A]` to an `Order[B]` using the given + * function `f`. + */ + def by[@sp A, @sp B](f: A => B)(implicit ev: Order[B]): Order[A] = + ev.on(f) + + /** + * Define an `Order[A]` using the given function `f`. + */ + def from[@sp A](f: (A, A) => Int): Order[A] = + new Order[A] { + def compare(x: A, y: A) = f(x, y) + } + + /** + * Implicitly convert a `Order[A]` to a `scala.math.Ordering[A]` + * instance. + */ + implicit def ordering[A](implicit ev: Order[A]): Ordering[A] = + ev.toOrdering + + /** + * An `Order` instance that considers all `A` instances to be equal. + */ + def allEqual[A]: Order[A] = new Order[A] { + def compare(x: A, y: A): Int = 0 + } + + + /** + * A `Monoid[Order[A]]` can be generated for all `A` with the following + * properties: + * + * `empty` returns a trivial `Order[A]` which considers all `A` instances to + * be equal. + * + * `combine(x: Order[A], y: Order[A])` creates an `Order[A]` that first + * orders by `x` and then (if two elements are equal) falls back to `y`. + * + * @see [[Order.whenEqual]] + */ + def whenEqualMonoid[A]: Monoid[Order[A]] = + new Monoid[Order[A]] { + val empty: Order[A] = allEqual[A] + + def combine(x: Order[A], y: Order[A]): Order[A] = x whenEqual y + } + + def fromOrdering[A](implicit ev: Ordering[A]): Order[A] = new Order[A] { + def compare(x: A, y: A): Int = ev.compare(x, y) + + override def toOrdering: Ordering[A] = ev + } +} diff --git a/kernel/src/main/scala/cats/PartialOrder.scala b/kernel/src/main/scala/cats/PartialOrder.scala new file mode 100644 index 0000000000..c35a1f9866 --- /dev/null +++ b/kernel/src/main/scala/cats/PartialOrder.scala @@ -0,0 +1,169 @@ +package cats + +import scala.{specialized => sp} + +/** + * The `PartialOrder` type class is used to define a partial ordering on some type `A`. + * + * A partial order is defined by a relation <=, which obeys the following laws: + * + * - x <= x (reflexivity) + * - if x <= y and y <= x, then x = y (anti-symmetry) + * - if x <= y and y <= z, then x <= z (transitivity) + * + * To compute both <= and >= at the same time, we use a Double number + * to encode the result of the comparisons x <= y and x >= y. + * The truth table is defined as follows: + * + * x <= y x >= y Double + * true true = 0.0 (corresponds to x = y) + * false false = NaN (x and y cannot be compared) + * true false = -1.0 (corresponds to x < y) + * false true = 1.0 (corresponds to x > y) + */ +trait PartialOrder[@sp A] extends Any with Eq[A] { self => + + /** + * Result of comparing `x` with `y`. Returns NaN if operands are not + * comparable. If operands are comparable, returns a Double whose + * sign is: + * - negative iff `x < y` + * - zero iff `x = y` + * - positive iff `x > y` + */ + def partialCompare(x: A, y: A): Double + + /** + * Result of comparing `x` with `y`. Returns None if operands are + * not comparable. If operands are comparable, returns Some[Int] + * where the Int sign is: + * - negative iff `x < y` + * - zero iff `x = y` + * - positive iff `x > y` + */ + def tryCompare(x: A, y: A): Option[Int] = { + val c = partialCompare(x, y) + if (c.isNaN) None else Some(c.signum) + } + + /** + * Returns Some(x) if x <= y, Some(y) if x > y, otherwise None. + */ + def pmin(x: A, y: A): Option[A] = { + val c = partialCompare(x, y) + if (c <= 0) Some(x) + else if (c > 0) Some(y) + else None + } + + /** + * Returns Some(x) if x >= y, Some(y) if x < y, otherwise None. + */ + def pmax(x: A, y: A): Option[A] = { + val c = partialCompare(x, y) + if (c >= 0) Some(x) + else if (c < 0) Some(y) + else None + } + + /** + * Defines a partial order on `B` by mapping `B` to `A` using `f` + * and using `A`s order to order `B`. + */ + override def on[@sp B](f: B => A): PartialOrder[B] = + new PartialOrder[B] { + def partialCompare(x: B, y: B): Double = self.partialCompare(f(x), f(y)) + } + + /** + * Defines a partial order on `A` where all arrows switch direction. + */ + def reverse: PartialOrder[A] = + new PartialOrder[A] { + def partialCompare(x: A, y: A): Double = self.partialCompare(y, x) + } + + // The following may be overridden for performance: + + /** + * Returns true if `x` = `y`, false otherwise. + */ + def eqv(x: A, y: A): Boolean = partialCompare(x, y) == 0 + + /** + * Returns true if `x` <= `y`, false otherwise. + */ + def lteqv(x: A, y: A): Boolean = partialCompare(x, y) <= 0 + + /** + * Returns true if `x` < `y`, false otherwise. + */ + def lt(x: A, y: A): Boolean = partialCompare(x, y) < 0 + + /** + * Returns true if `x` >= `y`, false otherwise. + */ + def gteqv(x: A, y: A): Boolean = partialCompare(x, y) >= 0 + + /** + * Returns true if `x` > `y`, false otherwise. + */ + def gt(x: A, y: A): Boolean = partialCompare(x, y) > 0 +} + +trait PartialOrderFunctions { + + def partialCompare[@sp A](x: A, y: A)(implicit ev: PartialOrder[A]): Double = + ev.partialCompare(x, y) + def tryCompare[@sp A](x: A, y: A)(implicit ev: PartialOrder[A]): Option[Int] = + ev.tryCompare(x, y) + + def pmin[@sp A](x: A, y: A)(implicit ev: PartialOrder[A]): Option[A] = + ev.pmin(x, y) + def pmax[@sp A](x: A, y: A)(implicit ev: PartialOrder[A]): Option[A] = + ev.pmax(x, y) + + def eqv[@sp A](x: A, y: A)(implicit ev: PartialOrder[A]): Boolean = + ev.eqv(x, y) + def lteqv[@sp A](x: A, y: A)(implicit ev: PartialOrder[A]): Boolean = + ev.lteqv(x, y) + def lt[@sp A](x: A, y: A)(implicit ev: PartialOrder[A]): Boolean = + ev.lt(x, y) + def gteqv[@sp A](x: A, y: A)(implicit ev: PartialOrder[A]): Boolean = + ev.gteqv(x, y) + def gt[@sp A](x: A, y: A)(implicit ev: PartialOrder[A]): Boolean = + ev.gt(x, y) +} + +object PartialOrder extends PartialOrderFunctions { + + /** + * Access an implicit `Eq[A]`. + */ + @inline final def apply[A](implicit ev: PartialOrder[A]) = ev + + /** + * Convert an implicit `PartialOrder[A]` to an `PartialOrder[B]` + * using the given function `f`. + */ + def by[@sp A, @sp B](f: A => B)(implicit ev: PartialOrder[B]): PartialOrder[A] = + ev.on(f) + + /** + * Define a `PartialOrder[A]` using the given function `f`. + */ + def from[@sp A](f: (A, A) => Double): PartialOrder[A] = + new PartialOrder[A] { + def partialCompare(x: A, y: A) = f(x, y) + } + + /** + * Implicitly convert a `PartialOrder[A]` to a + * `scala.math.PartialOrdering[A]` instance. + */ + implicit def partialOrdering[A](implicit ev: PartialOrder[A]): PartialOrdering[A] = + new PartialOrdering[A] { + def tryCompare(x: A, y: A): Option[Int] = ev.tryCompare(x, y) + def lteq(x: A, y: A): Boolean = ev.lteqv(x, y) + } +} diff --git a/kernel/src/main/scala/cats/Semigroup.scala b/kernel/src/main/scala/cats/Semigroup.scala new file mode 100644 index 0000000000..f59e90968a --- /dev/null +++ b/kernel/src/main/scala/cats/Semigroup.scala @@ -0,0 +1,73 @@ +package cats + +import scala.{ specialized => sp } +import scala.annotation.{ switch, tailrec } + +/** + * A semigroup is any set `A` with an associative operation (`combine`). + */ +trait Semigroup[@sp(Int, Long, Float, Double) A] extends Any with Serializable { + + /** + * Associative operation taking which combines two values. + */ + def combine(x: A, y: A): A + + /** + * Return `a` combined with itself `n` times. + */ + def combineN(a: A, n: Int): A = + if (n <= 0) throw new IllegalArgumentException("Repeated combining for semigroups must have n > 0") + else repeatedCombineN(a, n) + + /** + * Return `a` combined with itself more than once. + */ + protected[this] def repeatedCombineN(a: A, n: Int): A = { + @tailrec def loop(b: A, k: Int, extra: A): A = + if (k == 1) combine(b, extra) else { + val x = if ((k & 1) == 1) combine(b, extra) else extra + loop(combine(b, b), k >>> 1, x) + } + if (n == 1) a else loop(a, n - 1, a) + } + + /** + * Given a sequence of `as`, combine them and return the total. + * + * If the sequence is empty, returns None. Otherwise, returns Some(total). + */ + def combineAllOption(as: TraversableOnce[A]): Option[A] = + as.reduceOption(combine) +} + +trait SemigroupFunctions[S[T] <: Semigroup[T]] { + def combine[@sp(Int, Long, Float, Double) A](x: A, y: A)(implicit ev: S[A]): A = + ev.combine(x, y) + + def maybeCombine[@sp(Int, Long, Float, Double) A](ox: Option[A], y: A)(implicit ev: S[A]): A = + ox match { + case Some(x) => ev.combine(x, y) + case None => y + } + + def maybeCombine[@sp(Int, Long, Float, Double) A](x: A, oy: Option[A])(implicit ev: S[A]): A = + oy match { + case Some(y) => ev.combine(x, y) + case None => x + } + + def combineN[@sp(Int, Long, Float, Double) A](a: A, n: Int)(implicit ev: S[A]): A = + ev.combineN(a, n) + + def combineAllOption[A](as: TraversableOnce[A])(implicit ev: S[A]): Option[A] = + ev.combineAllOption(as) +} + +object Semigroup extends SemigroupFunctions[Semigroup] { + + /** + * Access an implicit `Semigroup[A]`. + */ + @inline final def apply[A](implicit ev: Semigroup[A]) = ev +} diff --git a/laws/src/main/scala/cats/laws/GroupLaws.scala b/laws/src/main/scala/cats/laws/GroupLaws.scala new file mode 100644 index 0000000000..05e24bf681 --- /dev/null +++ b/laws/src/main/scala/cats/laws/GroupLaws.scala @@ -0,0 +1,68 @@ +package cats +package laws + +import org.typelevel.discipline.Laws + +import org.scalacheck.{Arbitrary, Prop} + +object GroupLaws { + def apply[A : Eq : Arbitrary] = new GroupLaws[A] { + def Equ = Eq[A] + def Arb = implicitly[Arbitrary[A]] + } +} + +trait GroupLaws[A] extends Laws { + + implicit def Equ: Eq[A] + implicit def Arb: Arbitrary[A] + + // groups + + def semigroup(implicit A: Semigroup[A]) = new GroupProperties( + name = "semigroup", + parents = Nil, + Rules.serializable(A), + Rules.associativity(A.combine), + Rules.repeat1("combineN")(A.combineN), + Rules.repeat2("combineN", "|+|")(A.combineN)(A.combine) + ) + + def monoid(implicit A: Monoid[A]) = new GroupProperties( + name = "monoid", + parents = List(semigroup), + Rules.leftIdentity(A.empty)(A.combine), + Rules.rightIdentity(A.empty)(A.combine), + Rules.repeat0("combineN", "id", A.empty)(A.combineN), + Rules.collect0("combineAll", "id", A.empty)(A.combineAll), + Rules.isId("isEmpty", A.empty)(A.isEmpty) + ) + + def group(implicit A: Group[A]) = new GroupProperties( + name = "group", + parents = List(monoid), + Rules.leftInverse(A.empty)(A.combine)(A.inverse), + Rules.rightInverse(A.empty)(A.combine)(A.inverse), + Rules.consistentInverse("remove")(A.remove)(A.combine)(A.inverse) + ) + + // property classes + + class GroupProperties( + val name: String, + val parents: Seq[GroupProperties], + val props: (String, Prop)* + ) extends RuleSet { + val bases = Nil + } + + class AdditiveProperties( + val base: GroupProperties, + val parents: Seq[AdditiveProperties], + val props: (String, Prop)* + ) extends RuleSet { + val name = base.name + val bases = List("base" -> base) + } + +} diff --git a/laws/src/main/scala/cats/laws/OrderLaws.scala b/laws/src/main/scala/cats/laws/OrderLaws.scala new file mode 100644 index 0000000000..8bb16b0dde --- /dev/null +++ b/laws/src/main/scala/cats/laws/OrderLaws.scala @@ -0,0 +1,79 @@ +package cats +package laws + +import org.typelevel.discipline.Laws + +import org.scalacheck.{Arbitrary, Prop} +import org.scalacheck.Prop._ + +import cats.std.boolean._ + +object OrderLaws { + def apply[A: Eq: Arbitrary] = new OrderLaws[A] { + def Equ = Eq[A] + def Arb = implicitly[Arbitrary[A]] + } +} + +trait OrderLaws[A] extends Laws { + + implicit def Equ: Eq[A] + implicit def Arb: Arbitrary[A] + + def eqv = new OrderProperties( + name = "eq", + parent = None, + Rules.serializable(Equ), + "reflexitivity-eq" -> forAll { (x: A) => + x ?== x + }, + "symmetry-eq" -> forAll { (x: A, y: A) => + Equ.eqv(x, y) ?== Equ.eqv(y, x) + }, + "antisymmetry-eq" -> forAll { (x: A, y: A, f: A => A) => + !Equ.eqv(x, y) ?|| Equ.eqv(f(x), f(y)) + }, + "transitivity-eq" -> forAll { (x: A, y: A, z: A) => + !(Equ.eqv(x, y) && Equ.eqv(y, z)) ?|| Equ.eqv(x, z) + } + ) + + def partialOrder(implicit A: PartialOrder[A]) = new OrderProperties( + name = "partialOrder", + parent = Some(eqv), + Rules.serializable(A), + "reflexitivity" -> forAll { (x: A) => + x ?<= x + }, + "antisymmetry" -> forAll { (x: A, y: A) => + !(A.lteqv(x, y) && A.lteqv(y, x)) ?|| A.eqv(x, y) + }, + "transitivity" -> forAll { (x: A, y: A, z: A) => + !(A.lteqv(x, y) && A.lteqv(y, z)) ?|| A.lteqv(x, z) + }, + "gteqv" -> forAll { (x: A, y: A) => + A.lteqv(x, y) ?== A.gteqv(y, x) + }, + "lt" -> forAll { (x: A, y: A) => + A.lt(x, y) ?== (A.lteqv(x, y) && A.neqv(x, y)) + }, + "gt" -> forAll { (x: A, y: A) => + A.lt(x, y) ?== A.gt(y, x) + } + ) + + def order(implicit A: Order[A]) = new OrderProperties( + name = "order", + parent = Some(partialOrder), + "totality" -> forAll { (x: A, y: A) => + A.lteqv(x, y) ?|| A.lteqv(y, x) + } + ) + + class OrderProperties( + name: String, + parent: Option[RuleSet], + props: (String, Prop)* + ) extends DefaultRuleSet(name, parent, props: _*) + +} diff --git a/laws/src/main/scala/cats/laws/Rules.scala b/laws/src/main/scala/cats/laws/Rules.scala new file mode 100644 index 0000000000..885c52e53c --- /dev/null +++ b/laws/src/main/scala/cats/laws/Rules.scala @@ -0,0 +1,98 @@ +package cats +package laws + +import org.scalacheck.{Arbitrary, Prop} +import org.scalacheck.Prop._ +import Prop.{False, Proof, Result} + +import cats.std.boolean._ + +object Rules { + + def serializable[M](m: M): (String, Prop) = "serializable" -> SerializableLaws.serializable(m) + + // Comparison operators for testing are supplied by CheckEqOps and + // CheckOrderOps in package.scala. They are: + // + // ?== Ensure that x equals y + // ?!= Ensure that x does not equal y + // ?< Ensure that x < y + // ?<= Ensure that x <= y + // ?> Ensure that x > y + // ?>= Ensure that x >= y + // + // The reason to prefer these operators is that when tests fail, we + // will get more detaild output about what the failing values were + // (in addition to the input values generated by ScalaCheck). + + def associativity[A: Arbitrary: Eq](f: (A, A) => A): (String, Prop) = + "associativity" -> forAll { (x: A, y: A, z: A) => + f(f(x, y), z) ?== f(x, f(y, z)) + } + + def leftIdentity[A: Arbitrary: Eq](id: A)(f: (A, A) => A): (String, Prop) = + "leftIdentity" -> forAll { (x: A) => + f(id, x) ?== x + } + + def rightIdentity[A: Arbitrary: Eq](id: A)(f: (A, A) => A): (String, Prop) = + "rightIdentity" -> forAll { (x: A) => + f(x, id) ?== x + } + + def leftInverse[A: Arbitrary: Eq](id: A)(f: (A, A) => A)(inv: A => A): (String, Prop) = + "left inverse" -> forAll { (x: A) => + id ?== f(inv(x), x) + } + + def rightInverse[A: Arbitrary: Eq](id: A)(f: (A, A) => A)(inv: A => A): (String, Prop) = + "right inverse" -> forAll { (x: A) => + id ?== f(x, inv(x)) + } + + def commutative[A: Arbitrary: Eq](f: (A, A) => A): (String, Prop) = + "commutative" -> forAll { (x: A, y: A) => + f(x, y) ?== f(y, x) + } + + def idempotence[A: Arbitrary: Eq](f: (A, A) => A): (String, Prop) = + "idempotence" -> forAll { (x: A) => + f(x, x) ?== x + } + + def consistentInverse[A: Arbitrary: Eq](name: String)(m: (A, A) => A)(f: (A, A) => A)(inv: A => A): (String, Prop) = + s"consistent $name" -> forAll { (x: A, y: A) => + m(x, y) ?== f(x, inv(y)) + } + + def repeat0[A: Arbitrary: Eq](name: String, sym: String, id: A)(r: (A, Int) => A): (String, Prop) = + s"$name(a, 0) == $sym" -> forAll { (a: A) => + r(a, 0) ?== id + } + + def repeat1[A: Arbitrary: Eq](name: String)(r: (A, Int) => A): (String, Prop) = + s"$name(a, 1) == a" -> forAll { (a: A) => + r(a, 1) ?== a + } + + def repeat2[A: Arbitrary: Eq](name: String, sym: String)(r: (A, Int) => A)(f: (A, A) => A): (String, Prop) = + s"$name(a, 2) == a $sym a" -> forAll { (a: A) => + r(a, 2) ?== f(a, a) + } + + def collect0[A: Arbitrary: Eq](name: String, sym: String, id: A)(c: Seq[A] => A): (String, Prop) = + s"$name(Nil) == $sym" -> forAll { (a: A) => + c(Nil) ?== id + } + + def isId[A: Arbitrary: Eq](name: String, id: A)(p: A => Boolean): (String, Prop) = + name -> forAll { (x: A) => + Eq.eqv(x, id) ?== p(x) + } + + def distributive[A: Arbitrary: Eq](a: (A, A) => A)(m: (A, A) => A): (String, Prop) = + "distributive" → forAll { (x: A, y: A, z: A) => + (m(x, a(y, z)) ?== a(m(x, y), m(x, z))) && + (m(a(x, y), z) ?== a(m(x, z), m(y, z))) + } +} diff --git a/laws/src/main/scala/cats/laws/discipline/Eq.scala b/laws/src/main/scala/cats/laws/discipline/Eq.scala index d79a1a6a84..ad4ba25219 100644 --- a/laws/src/main/scala/cats/laws/discipline/Eq.scala +++ b/laws/src/main/scala/cats/laws/discipline/Eq.scala @@ -2,7 +2,6 @@ package cats package laws package discipline -import algebra.Eq import org.scalacheck.Arbitrary object eq { diff --git a/laws/src/main/scala/cats/laws/discipline/MonoidalTests.scala b/laws/src/main/scala/cats/laws/discipline/MonoidalTests.scala index 9c836c64f2..79e32eb712 100644 --- a/laws/src/main/scala/cats/laws/discipline/MonoidalTests.scala +++ b/laws/src/main/scala/cats/laws/discipline/MonoidalTests.scala @@ -34,13 +34,12 @@ object MonoidalTests { } object Isomorphisms { - import algebra.laws._ implicit def invariant[F[_]](implicit F: functor.Invariant[F]): Isomorphisms[F] = new Isomorphisms[F] { def associativity[A, B, C](fs: (F[(A, (B, C))], F[((A, B), C)]))(implicit EqFABC: Eq[F[(A, B, C)]]) = - F.imap(fs._1) { case (a, (b, c)) => (a, b, c) } { case (a, b, c) => (a, (b, c)) } ?== + F.imap(fs._1) { case (a, (b, c)) => (a, b, c) } { case (a, b, c) => (a, (b, c)) } <-> F.imap(fs._2) { case ((a, b), c) => (a, b, c) } { case (a, b, c) => ((a, b), c) } } } -} \ No newline at end of file +} diff --git a/laws/src/main/scala/cats/laws/discipline/package.scala b/laws/src/main/scala/cats/laws/discipline/package.scala index a015ad9bcd..de5f10dd6f 100644 --- a/laws/src/main/scala/cats/laws/discipline/package.scala +++ b/laws/src/main/scala/cats/laws/discipline/package.scala @@ -1,8 +1,6 @@ package cats package laws -import algebra.laws._ - import org.scalacheck.Prop package object discipline { diff --git a/laws/src/main/scala/cats/laws/package.scala b/laws/src/main/scala/cats/laws/package.scala index fbfcfaf7d7..5965ef27a1 100644 --- a/laws/src/main/scala/cats/laws/package.scala +++ b/laws/src/main/scala/cats/laws/package.scala @@ -1,7 +1,41 @@ package cats +import org.scalacheck._ +import org.scalacheck.util.Pretty +import Prop.{False, Proof, Result} + package object laws { implicit final class IsEqArrow[A](val lhs: A) extends AnyVal { def <->(rhs: A): IsEq[A] = IsEq(lhs, rhs) } + + lazy val proved = Prop(Result(status = Proof)) + + lazy val falsified = Prop(Result(status = False)) + + object Ops { + def run[A](sym: String)(lhs: A, rhs: A)(f: (A, A) => Boolean): Prop = + if (f(lhs, rhs)) proved else falsified :| { + val exp = Pretty.pretty(lhs, Pretty.Params(0)) + val got = Pretty.pretty(rhs, Pretty.Params(0)) + s"($exp $sym $got) failed" + } + } + + implicit class CheckEqOps[A](lhs: A)(implicit ev: Eq[A], pp: A => Pretty) { + def ?==(rhs: A): Prop = Ops.run("?==")(lhs, rhs)(ev.eqv) + def ?!=(rhs: A): Prop = Ops.run("?!=")(lhs, rhs)(ev.neqv) + } + + implicit class CheckOrderOps[A](lhs: A)(implicit ev: PartialOrder[A], pp: A => Pretty) { + def ?<(rhs: A): Prop = Ops.run("?<")(lhs, rhs)(ev.lt) + def ?<=(rhs: A): Prop = Ops.run("?<=")(lhs, rhs)(ev.lteqv) + def ?>(rhs: A): Prop = Ops.run("?>")(lhs, rhs)(ev.gt) + def ?>=(rhs: A): Prop = Ops.run("?>=")(lhs, rhs)(ev.gteqv) + } + + implicit class BooleanOps[A](lhs: Boolean)(implicit pp: Boolean => Pretty) { + def ?&&(rhs: Boolean): Prop = Ops.run("?&&")(lhs, rhs)(_ && _) + def ?||(rhs: Boolean): Prop = Ops.run("?||")(lhs, rhs)(_ || _) + } } diff --git a/tests/src/test/scala/cats/tests/ConstTests.scala b/tests/src/test/scala/cats/tests/ConstTests.scala index c52482425a..73092ddf3f 100644 --- a/tests/src/test/scala/cats/tests/ConstTests.scala +++ b/tests/src/test/scala/cats/tests/ConstTests.scala @@ -1,10 +1,10 @@ package cats package tests -import algebra.laws.{GroupLaws, OrderLaws} import cats.data.{Const, NonEmptyList} import cats.functor.Contravariant +import cats.laws.{GroupLaws, OrderLaws} import cats.laws.discipline._ import cats.laws.discipline.arbitrary.{constArbitrary, oneAndArbitrary} diff --git a/tests/src/test/scala/cats/tests/CoproductTests.scala b/tests/src/test/scala/cats/tests/CoproductTests.scala index 7435cd3bfb..2f5ec19cf7 100644 --- a/tests/src/test/scala/cats/tests/CoproductTests.scala +++ b/tests/src/test/scala/cats/tests/CoproductTests.scala @@ -1,10 +1,9 @@ package cats.tests -import algebra.Eq -import algebra.laws.OrderLaws import cats._ import cats.data.Coproduct import cats.functor.Contravariant +import cats.laws.OrderLaws import cats.laws.discipline._ import cats.laws.discipline.arbitrary._ import org.scalacheck.Arbitrary diff --git a/tests/src/test/scala/cats/tests/EitherTests.scala b/tests/src/test/scala/cats/tests/EitherTests.scala index 5c4a015968..c587edbffc 100644 --- a/tests/src/test/scala/cats/tests/EitherTests.scala +++ b/tests/src/test/scala/cats/tests/EitherTests.scala @@ -1,9 +1,9 @@ package cats package tests +import cats.laws.OrderLaws import cats.laws.discipline.{TraverseTests, MonadTests, SerializableTests, MonoidalTests} import cats.laws.discipline.eq._ -import algebra.laws.OrderLaws class EitherTests extends CatsSuite { diff --git a/tests/src/test/scala/cats/tests/EvalTests.scala b/tests/src/test/scala/cats/tests/EvalTests.scala index 7c40556908..28fe14a249 100644 --- a/tests/src/test/scala/cats/tests/EvalTests.scala +++ b/tests/src/test/scala/cats/tests/EvalTests.scala @@ -2,11 +2,10 @@ package cats package tests import scala.math.min -import cats.laws.ComonadLaws +import cats.laws.{ComonadLaws, GroupLaws, OrderLaws} import cats.laws.discipline.{MonoidalTests, BimonadTests, SerializableTests} import cats.laws.discipline.arbitrary._ import cats.laws.discipline.eq._ -import algebra.laws.{GroupLaws, OrderLaws} class EvalTests extends CatsSuite { diff --git a/tests/src/test/scala/cats/tests/FunctionTests.scala b/tests/src/test/scala/cats/tests/FunctionTests.scala index 185dc8eaab..e81575e6a4 100644 --- a/tests/src/test/scala/cats/tests/FunctionTests.scala +++ b/tests/src/test/scala/cats/tests/FunctionTests.scala @@ -5,10 +5,10 @@ import org.scalacheck.Arbitrary import cats.arrow.{Arrow, Choice} import cats.functor.Contravariant +import cats.laws.GroupLaws import cats.laws.discipline._ import cats.laws.discipline.eq._ import cats.laws.discipline.arbitrary._ -import algebra.laws.GroupLaws class FunctionTests extends CatsSuite { diff --git a/tests/src/test/scala/cats/tests/KleisliTests.scala b/tests/src/test/scala/cats/tests/KleisliTests.scala index 9c5f6cea22..9fc1fd8a03 100644 --- a/tests/src/test/scala/cats/tests/KleisliTests.scala +++ b/tests/src/test/scala/cats/tests/KleisliTests.scala @@ -4,11 +4,11 @@ package tests import cats.arrow.{Arrow, Choice, Split} import cats.data.{Kleisli, Reader} import cats.functor.{Contravariant, Strong} +import cats.laws.GroupLaws import cats.laws.discipline._ import cats.laws.discipline.arbitrary._ import cats.laws.discipline.eq._ import org.scalacheck.Arbitrary -import algebra.laws.GroupLaws import cats.laws.discipline.{SemigroupKTests, MonoidKTests} class KleisliTests extends CatsSuite { diff --git a/tests/src/test/scala/cats/tests/OneAndTests.scala b/tests/src/test/scala/cats/tests/OneAndTests.scala index 0b6f5c3ea0..30fdd4f863 100644 --- a/tests/src/test/scala/cats/tests/OneAndTests.scala +++ b/tests/src/test/scala/cats/tests/OneAndTests.scala @@ -1,9 +1,8 @@ package cats package tests -import algebra.laws.{GroupLaws, OrderLaws} - import cats.data.{NonEmptyList, OneAnd} +import cats.laws.{GroupLaws, OrderLaws} import cats.laws.discipline.{ComonadTests, FunctorTests, SemigroupKTests, FoldableTests, MonadTests, SerializableTests, MonoidalTests} import cats.laws.discipline.arbitrary.{evalArbitrary, oneAndArbitrary} import cats.laws.discipline.eq._ diff --git a/tests/src/test/scala/cats/tests/SetTests.scala b/tests/src/test/scala/cats/tests/SetTests.scala index 571b238b0b..d32ce8d01b 100644 --- a/tests/src/test/scala/cats/tests/SetTests.scala +++ b/tests/src/test/scala/cats/tests/SetTests.scala @@ -4,7 +4,7 @@ package tests import cats.laws.discipline.{FoldableTests, MonoidKTests, SerializableTests} class SetTests extends CatsSuite { - checkAll("Set[Int]", algebra.laws.GroupLaws[Set[Int]].monoid) + checkAll("Set[Int]", cats.laws.GroupLaws[Set[Int]].monoid) checkAll("Set[Int]", MonoidKTests[Set].monoidK[Int]) checkAll("MonoidK[Set]", SerializableTests.serializable(MonoidK[Set])) diff --git a/tests/src/test/scala/cats/tests/StreamingTTests.scala b/tests/src/test/scala/cats/tests/StreamingTTests.scala index 753bdae828..b18beda640 100644 --- a/tests/src/test/scala/cats/tests/StreamingTTests.scala +++ b/tests/src/test/scala/cats/tests/StreamingTTests.scala @@ -1,9 +1,8 @@ package cats package tests -import algebra.laws.OrderLaws - import cats.data.{Streaming, StreamingT} +import cats.laws.OrderLaws import cats.laws.discipline.{MonoidalTests, CoflatMapTests, MonadCombineTests, SerializableTests} import cats.laws.discipline.arbitrary._ import cats.laws.discipline.eq._ diff --git a/tests/src/test/scala/cats/tests/StreamingTests.scala b/tests/src/test/scala/cats/tests/StreamingTests.scala index 3097c5ce35..9dbb7568dc 100644 --- a/tests/src/test/scala/cats/tests/StreamingTests.scala +++ b/tests/src/test/scala/cats/tests/StreamingTests.scala @@ -1,9 +1,8 @@ package cats package tests -import algebra.laws.OrderLaws - import cats.data.Streaming +import cats.laws.OrderLaws import cats.laws.discipline.arbitrary._ import cats.laws.discipline.eq._ import cats.laws.discipline.{TraverseTests, CoflatMapTests, MonadCombineTests, SerializableTests, MonoidalTests} diff --git a/tests/src/test/scala/cats/tests/SyntaxTests.scala b/tests/src/test/scala/cats/tests/SyntaxTests.scala index 098ed3e359..19fd969de1 100644 --- a/tests/src/test/scala/cats/tests/SyntaxTests.scala +++ b/tests/src/test/scala/cats/tests/SyntaxTests.scala @@ -3,8 +3,8 @@ package tests import cats.std.AllInstances import cats.syntax.AllSyntax -import algebra.laws.GroupLaws import cats.functor.{Invariant, Contravariant} +import cats.laws.GroupLaws import cats.laws.discipline.SerializableTests import org.scalacheck.{Arbitrary} diff --git a/tests/src/test/scala/cats/tests/ValidatedTests.scala b/tests/src/test/scala/cats/tests/ValidatedTests.scala index ea575a8d86..6d608b3b9e 100644 --- a/tests/src/test/scala/cats/tests/ValidatedTests.scala +++ b/tests/src/test/scala/cats/tests/ValidatedTests.scala @@ -6,9 +6,9 @@ import cats.data.Validated.{Valid, Invalid} import cats.laws.discipline.{BifunctorTests, TraverseTests, ApplicativeTests, SerializableTests, MonoidalTests} import org.scalacheck.{Gen, Arbitrary} import org.scalacheck.Arbitrary._ +import cats.laws.{OrderLaws, GroupLaws} import cats.laws.discipline.arbitrary._ import cats.laws.discipline.eq.tuple3Eq -import algebra.laws.{OrderLaws, GroupLaws} import scala.util.Try diff --git a/tests/src/test/scala/cats/tests/WriterTTests.scala b/tests/src/test/scala/cats/tests/WriterTTests.scala index d214a08899..da1f13cdf5 100644 --- a/tests/src/test/scala/cats/tests/WriterTTests.scala +++ b/tests/src/test/scala/cats/tests/WriterTTests.scala @@ -3,11 +3,11 @@ package tests import cats.data.{Writer, WriterT} import cats.functor.Bifunctor +import cats.laws.OrderLaws import cats.laws.discipline._ import cats.laws.discipline.eq._ import cats.laws.discipline.arbitrary._ -import algebra.laws.OrderLaws import org.scalacheck.Prop.forAll class WriterTTests extends CatsSuite { diff --git a/tests/src/test/scala/cats/tests/XorTTests.scala b/tests/src/test/scala/cats/tests/XorTTests.scala index c24f1561ff..17404b837f 100644 --- a/tests/src/test/scala/cats/tests/XorTTests.scala +++ b/tests/src/test/scala/cats/tests/XorTTests.scala @@ -3,10 +3,10 @@ package tests import cats.functor.Bifunctor import cats.data.{Xor, XorT} +import cats.laws.OrderLaws import cats.laws.discipline._ import cats.laws.discipline.arbitrary._ import cats.laws.discipline.eq.tuple3Eq -import algebra.laws.OrderLaws class XorTTests extends CatsSuite { implicit val eq0 = XorT.xorTEq[List, String, String Xor Int] diff --git a/tests/src/test/scala/cats/tests/XorTests.scala b/tests/src/test/scala/cats/tests/XorTests.scala index e8aec6c09b..8ac4d9d022 100644 --- a/tests/src/test/scala/cats/tests/XorTests.scala +++ b/tests/src/test/scala/cats/tests/XorTests.scala @@ -3,10 +3,10 @@ package tests import cats.data.{NonEmptyList, Xor, XorT} import cats.data.Xor._ +import cats.laws.{GroupLaws, OrderLaws} import cats.laws.discipline.arbitrary._ import cats.laws.discipline.{BifunctorTests, TraverseTests, MonadErrorTests, SerializableTests, MonoidalTests} import cats.laws.discipline.eq.tuple3Eq -import algebra.laws.{GroupLaws, OrderLaws} import org.scalacheck.{Arbitrary, Gen} import org.scalacheck.Arbitrary._