├── .gitignore ├── project └── plugins.sbt ├── src ├── main │ └── scala │ │ └── principled │ │ └── LawSet.scala └── test │ └── scala │ └── principled │ └── LawSetTest.scala └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Sbt 2 | target/ 3 | 4 | # Eclipse 5 | .classpath 6 | .project 7 | .settings/ 8 | bin/ 9 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Resolver.sonatypeRepo("releases") 2 | 3 | addSbtPlugin("org.wartremover" % "sbt-wartremover" % "1.2.1") 4 | 5 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.8.0") 6 | -------------------------------------------------------------------------------- /src/main/scala/principled/LawSet.scala: -------------------------------------------------------------------------------- 1 | package principled 2 | 3 | import org.scalacheck.Prop 4 | import org.scalacheck.Properties 5 | 6 | abstract class LawSet(val name: String) { 7 | def bases: Seq[(String, LawSet)] 8 | def props: Seq[(String, Prop)] 9 | 10 | final def all: Properties = new Properties(name) { 11 | for { 12 | (laws, path) <- allLawSets 13 | (name, prop) <- laws.props 14 | } property(path + name) = prop 15 | } 16 | 17 | 18 | private type Path = List[String] 19 | def root: Path = Nil 20 | 21 | private lazy val allLawSets: Map[LawSet, String] = 22 | collect(root) 23 | .groupBy(_._1) 24 | .mapValues(_ map { _._2.mkString(".") }) 25 | .mapValues(_.sorted) 26 | .mapValues { 27 | case Seq("") => "" 28 | case Seq(x) => x + "." 29 | case seq => seq.mkString("{", ", ", "}.") 30 | } 31 | 32 | private def collect(prefix: Path): Seq[(LawSet, Path)] = 33 | Seq((this, prefix)) ++ bases.flatMap { case (name, base) => 34 | base.collect(prefix :+ s"${name}:${base.name}") } 35 | } -------------------------------------------------------------------------------- /src/test/scala/principled/LawSetTest.scala: -------------------------------------------------------------------------------- 1 | package principled 2 | 3 | import scala.annotation.elidable 4 | import scala.annotation.elidable.ASSERTION 5 | import org.scalacheck.Properties 6 | import org.scalacheck.Prop 7 | 8 | object LawSetTest extends Properties("LawSet") { 9 | 10 | private def dummyProp = Prop.passed 11 | 12 | property("sanity") = { 13 | 14 | val foo = new LawSet("Foo") { 15 | def bases = Seq() 16 | def props = Seq("foo1" -> dummyProp) 17 | } 18 | 19 | val bar = new LawSet("Bar") { 20 | def bases = Seq("base" -> foo) 21 | def props = Seq("bar1" -> dummyProp) 22 | } 23 | 24 | val baz = new LawSet("Baz") { 25 | def bases = Seq("base" -> foo) 26 | def props = Seq("baz1" -> dummyProp) 27 | } 28 | 29 | val qux = new LawSet("Qux") { 30 | def bases = Seq("bar" -> bar, "baz" -> baz) 31 | def props = Seq("qux1" -> dummyProp) 32 | } 33 | 34 | val all = qux.all.properties.map(_._1.split('.').last) 35 | 36 | Prop.all( 37 | "foo1 included" |: Prop(all.contains("foo1")), 38 | "no redundancy" |: Prop(all.count(_ == "foo1") <= 1), 39 | "overall" |: Prop(all.toSet == Set("foo1", "bar1", "baz1", "qux1")) 40 | ) 41 | } 42 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Principled 2 | ========== 3 | 4 | Principled is a thin add-on to [ScalaCheck](http://www.scalacheck.org/) for (algebra-like) law checking. It is an alternative to [discipline](https://github.com/typelevel/discipline). 5 | 6 | 7 | Purpose 8 | ------- 9 | 10 | The purpose of Principled is to allow law inheritance. This is common in algebraic structures: monoid inherits all laws of semigroup, and adds some more. In addition, we want to avoid checking the same law multiple times in case of diamond inheritance. 11 | 12 | Approach 13 | -------- 14 | 15 | Principled defines `LawSet`, a collections of laws (a law is just a named ScalaCheck property). A `LawSet` can have zero or more _base_ `LawSet`s. A `LawSet` inherits all of the laws from all of its bases, but removes duplicates. 16 | 17 | Example 18 | ------- 19 | 20 | ```scala 21 | case class SemigroupLaws[A: Arbitrary](S: Semigroup[A]) extends LawSet("Semigroup") { 22 | implicit def semigroup = S 23 | 24 | override val bases = Seq() // Semigroup does not inherit any laws 25 | 26 | // define Semigroup laws 27 | override val props = Seq( 28 | "associativity" -> forAll((a: A, b: A, c: A) => 29 | ((a |+| b) |+| c) == (a |+| (b |+| c))) 30 | ) 31 | } 32 | 33 | case class MonoidLaws[A: Arbitrary](M: Monoid[A]) extends LawSet("Monoid") { 34 | implicit def monoid = M 35 | 36 | override val bases = Seq("semigroup" -> SemigroupLaws(M)) // inherit Semigroup laws 37 | 38 | // add some more laws 39 | override val props = Seq( 40 | "leftIdentity" -> forAll((a: A) => 41 | (M.zero |+| a) == a), 42 | "rightIdentity" -> forAll((a: A) => 43 | (a |+| M.zero) == a)) 44 | } 45 | 46 | case class OrderLaws[A: Arbitrary](O: Order[A]) extends LawSet("Order") { 47 | implicit def order = O 48 | 49 | override def bases = Seq() // Order does not inherit any laws 50 | 51 | // define Order laws 52 | override val props = Seq( 53 | "reflexivity" -> forAll((a: A) => 54 | a lte a), 55 | "antisymmetry" -> forAll((a: A, b: A) => 56 | (a cmp b) == (b cmp a).complement), 57 | "transitivity" -> forAll((a: A, b: A, c: A) => { 58 | if(a lte b) 59 | (b lte c) ==> (a lte c) 60 | else // a > b 61 | (b gte c) ==> (a gte c) 62 | })) 63 | } 64 | 65 | case class OrderedSemigroupLaws[A: Arbitrary](S: OrderedSemigroup[A]) 66 | extends LawSet("OrderedSemigroup") { 67 | implicit def orderedSemigroup = S 68 | 69 | override val bases = Seq( 70 | "semigroup" -> SemigroupLaws[A](S), // inherit Semigroup laws 71 | "order" -> OrderLaws[A](S)) // inherit Order laws 72 | 73 | // add some more laws 74 | override val props = Seq( 75 | "leftCompatibility" -> forAll((a: A, b: A, c: A) => 76 | (a cmp b) == ((c |+| a) cmp (c |+| b))), 77 | "rightCompatibility" -> forAll((a: A, b: A, c: A) => 78 | (a cmp b) == ((a |+| c) cmp (b |+| c)))) 79 | } 80 | 81 | case class OrderedMonoidLaws[A: Arbitrary](M: OrderedMonoid[A]) extends LawSet("OrderedMonoid") { 82 | 83 | override val bases = Seq( 84 | "orderedSemigroup" -> OrderedSemigroupLaws(M), // inherit OrderedSemigroup laws 85 | "monoid" -> MonoidLaws(M)) // inherit Monoid laws 86 | 87 | // no additional laws 88 | override def props = Seq() 89 | } 90 | ``` 91 | 92 | Note that `OrderedMonoidLaws` inherits `SemigroupLaws` twice: once via `OrderedSemigroupLaws` and once via `MonoidLaws`. However, Principled makes sure that `SemigroupLaws` will be checked only once. 93 | 94 | Testing `OrderedMonoidLaws` of a particular instance of `OrderedMonoid`: 95 | 96 | ```scala 97 | object LawTests extends org.scalacheck.Properties("Laws") { 98 | 99 | val myOrderedMonoid: OrderedMonoid[A] = ??? 100 | 101 | include(OrderedMonoidLaws(myOrderedMonoid).all) 102 | 103 | } 104 | ``` 105 | 106 | 107 | Comparison to Discipline 108 | ------------------------ 109 | 110 | This project was motivated by the following shortcomings of Discipline. 111 | 112 | Discipline introduces quite a bit of _complexity_ in organizing the law inheritance hierarchy: 113 | - There are two types of ancestors: _parent_ and _base_, with different inheritance semantics. With such _complex inheritance semantics_, ensuring each law is checked exactly once likely requires you to know the whole inheritance hierarchy, thus defying local reasoning and being fragile with respect to future changes in the hierarchy. 114 | - You are required you to classify `RuleSet`s into _kinds_. Such classification is often _unnatural_. For example, [algebra](https://github.com/non/algebra) defines an _Order_ kind and a _Group_ kind. `OrderedSemigroupLaws` above does not fall into either of those kinds (more precisely, it falls equally well into both kinds). 115 | - _Not modular:_ Extending kinds is not robust, since property names within a kind have to be unique. 116 | - As a result of the above, your best bet to make sure you don't miss any law is to only use _base_ inheritance. That, however, does not avoid duplicates. 117 | 118 | Principled does not try to come up with a clever way of structuring your type classes/laws in order to avoid duplicates. Instead, it relies on testing equality of `LawSet`s. Notice in the example above that law sets are defined as case classes, which give a proper implementation of `==`. 119 | 120 | Try it out 121 | ---------- 122 | 123 | 1. **Publish locally** 124 | ```sh 125 | git clone https://github.com/TomasMikula/Principled.git 126 | cd Principled 127 | sbt publish-local 128 | ``` 129 | 130 | 2. **Add to dependencies** 131 | ```scala 132 | libraryDependencies ++= Seq( 133 | "org.principled" %% "principled" % "0.1-SNAPSHOT" 134 | ) 135 | ``` 136 | --------------------------------------------------------------------------------