WIP: Switch laws to cats-core encoding#206
Conversation
|
Am I correct in remembering that this is holding up the publish for cats 1.0-RC1? |
|
I just finished up the lattice laws and would love to get some feedback before I start with the Ring laws. :) |
| } | ||
| def absorption(x: A, y: A)(implicit E: Eq[A]): IsEq[Boolean] = | ||
| (E.eqv(S.join(x, S.meet(x, y)), x) && E.eqv(S.meet(x, S.join(x, y)), x)) <-> true | ||
| } |
There was a problem hiding this comment.
I would split the absorption law in two pieces, so that the return type is IsEq[A]
| def distributive(x: A, y: A, z: A)(implicit E: Eq[A]): IsEq[Boolean] = | ||
| (E.eqv(S.join(x, S.meet(y, z)), S.meet(S.join(x, y), S.join(x, z))) && | ||
| E.eqv(S.meet(x, S.join(y, z)), S.join(S.meet(x, y), S.meet(x, z)))) <-> true | ||
|
|
There was a problem hiding this comment.
Why not split in two laws that return IsEq[A]?
| import algebra.lattice.JoinSemilattice | ||
| import cats.kernel.laws.SemilatticeLaws | ||
|
|
||
| trait JoinSemilatticeLaws[A] extends SemilatticeLaws[A] { |
There was a problem hiding this comment.
A JoinSemilattice does not extend Semilattice, so why should the laws follow the same pattern?
|
Hi @LukaJCB , now I'm convinced that the new cats-core law encoding is bad for algebra/spire. It has a lot of code duplication between the discipline and non-discipline parts. The law delegation implemented in lattices and rings is only visible in the discipline package, which negates the clarity advantage of having straightforward declarations of laws without the discipline encoding. Anyway, I don't have a strong opinion for algebra as Spire is probably going to keep its own law implementations to cater for primitive types that approximate a lawful type (for example integers). |
|
Hey Denis, thanks for checking in! I think that's a good decision. We'll have to see on the algebra front (as it might move into the cats repo at some point) |
|
Personally, I think this whole discipline style is pretty terrible. I don't know why law methods returning scalacheck Prop on objects aren't actually better. |
|
@johnynek which style do you find terrible? the current algebra style, the cats-core style, both? What would you advocate? (I'm in the middle of refactoring laws for Spire, and am trying for find good middle ground) |
|
Frankly both. I would do: object Laws {
def associativity[T: Semigroup: Arbitrary: Eq]: Prop =
forAll { (a: T, b: T, c: T) =>
val right = Semigroup[T].combine(a, Semigroup[T].combine(b, c))
val left = Semigroup[T].combine(Semigroup[T].combine(a, b), c)
Eq[T].eqv(left, right)
} :| "Semigroup Associativity"
}etc... and I would build up to things like by calling the others. Then users can check the properties using scalacheck or scalatest (both can check Prop). I don't see the need to build this inheritance heavy framework around what should just be functions that return Prop. |
|
@johnynek How does it behave when checking the resulting |
|
I agree that discipline is pretty terrible. Some time ago I tried to summarize what bothered me with discipline. (The approach I implemented in that project is not great either—it uses |
|
@TomasMikula Actually, the |
|
It's not the only way, and it's not a simple one, either. Why do you need the distinction in this specific case of rings? |
|
I guess the original justification was to avoid duplication of the semigroup/monoid/group laws for the additive and multiplicative structures in a ring. The alternative is to have a hierarchy for additive structures, a hierarchy for multiplicative structures, and rings inherit both. I guess it's a tradeoff. |
|
I mean, why do you need the distinction between |
|
In I don't see a way out of having two kind of inheritance relations, short of duplicating laws. BTW @non is the original author of discipline; I'm merely trying to find an optimal way to test numerical stuff in Spire. As of now, all solutions I have seen have drawbacks. |
Principled avoids duplication with one kind of inheritance. One of the problems with discipline is that if I choose a |
|
In that case, you lose the possibility of checking both For that purpose, BTW, I don't think there is any use of |
It wouldn't be eliminated, because the two law sets for monoid would compare as different (because the two
Do cats use discipline much at all? |
|
@TomasMikula thanks for the clarification! |
|
@johnynek Thanks for intervening. We can indeed remove all abstractions. We pay a small price, the repetition of the I also removed the Parents are handled by merging object GroupLaws {
def semigroup[A:Arbitrary:Eq](implicit A: Semigroup[A]): Map[String, Prop] =
Map(
"Semigroup.associativity" -> forAll { (x: A, y: A, z: A) =>
A.combine(A.combine(x, y), z) ?== A.combine(x, A.combine(y, z))
},
"Semigroup.combineN(x, 1) === x" -> forAll { (x: A) =>
A.combineN(x, 1) ?== x
},
"Semigroup.combineN(x, 2) === x |+| x" -> forAll { (x: A) =>
A.combineN(x, 2) ?== A.combine(x, x)
},
"Semigroup.combineAllOption" -> forAll { (xs: Vector[A]) =>
A.combineAllOption(xs) ?== xs.reduceOption(A.combine)
}
)
def band[A:Arbitrary:Eq](implicit A: Band[A]): Map[String, Prop] =
semigroup[A] ++ Map(
"Band.idempotence" -> forAll { (x: A) =>
A.combine(x, x) ?== x
}
)
} |
|
Nice. Maybe don't add the |
|
The prefix is a convention to avoid name collisions. Now looking at a proof-of-concept translation of all algebra laws. The encoding above could be a tad slower for the big nested hierarchies. |
I mean, optionally add a prefix when inheriting. |
|
I want to avoid fancy abstraction as much as possible. With this encoding, given a failed test, a simple string search reveals the corresponding law. |
|
That's a good argument 👍 |
|
Closing this now :) |
This isn't close to finished, but should update the build to RC1 soon :)