Skip to content
This repository was archived by the owner on Feb 8, 2022. It is now read-only.

WIP: Switch laws to cats-core encoding#206

Closed
LukaJCB wants to merge 3 commits into
typelevel:masterfrom
LukaJCB:update-laws
Closed

WIP: Switch laws to cats-core encoding#206
LukaJCB wants to merge 3 commits into
typelevel:masterfrom
LukaJCB:update-laws

Conversation

@LukaJCB

@LukaJCB LukaJCB commented Nov 6, 2017

Copy link
Copy Markdown
Member

This isn't close to finished, but should update the build to RC1 soon :)

@coltfred

Copy link
Copy Markdown
Contributor

Am I correct in remembering that this is holding up the publish for cats 1.0-RC1?

@LukaJCB LukaJCB changed the title WIP: Switch to cats-core encoding WIP: Switch laws to cats-core encoding Nov 24, 2017
@LukaJCB

LukaJCB commented Nov 24, 2017

Copy link
Copy Markdown
Member Author

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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A JoinSemilattice does not extend Semilattice, so why should the laws follow the same pattern?

@johnynek johnynek mentioned this pull request Nov 27, 2017
@denisrosset

Copy link
Copy Markdown
Contributor

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).

@LukaJCB

LukaJCB commented Dec 1, 2017

Copy link
Copy Markdown
Member Author

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)

@johnynek

johnynek commented Dec 1, 2017

Copy link
Copy Markdown
Contributor

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.

@denisrosset

Copy link
Copy Markdown
Contributor

@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)

@johnynek

johnynek commented Dec 1, 2017

Copy link
Copy Markdown
Contributor

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

def monoidLaws[T: Monoid: Arbitrary: Eq]: Prop

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.

@denisrosset

Copy link
Copy Markdown
Contributor

@johnynek How does it behave when checking the resulting Prop? Does it perform minSuccessful tests for each embedded property, or distributes the 100 cases randomly among the checked properties? Do you have a code example written in that style?

@TomasMikula

Copy link
Copy Markdown
Contributor

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 == on "law sets" to avoid duplicate laws in case there is diamond inheritance.) Maybe it's really better to explicitly state all the laws of a type class without inheriting any (but reusing implementation).

@denisrosset

Copy link
Copy Markdown
Contributor

@TomasMikula Actually, the parents vs bases distinction enables the reuse of the semigroup/monoid/group laws to test the additive and multiplicative substructures of rings. So this complexity has a justification.

@TomasMikula

Copy link
Copy Markdown
Contributor

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?

@denisrosset

Copy link
Copy Markdown
Contributor

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.

@TomasMikula

Copy link
Copy Markdown
Contributor

I mean, why do you need the distinction between parents and bases? Why not have just one type of inheritance? I guess the motivation there was to avoid inheriting the same laws twice in case of diamond inheritance (such as MonadLaws inheriting functor laws twice, once from ApplicativeLaws and once from FlatMapLaws), but I don't think it is a good solution (for reasons listed in the above link).

@denisrosset

denisrosset commented Dec 1, 2017

Copy link
Copy Markdown
Contributor

In parents, you avoid checking laws twice, which corresponds to the case you mention. In bases, you allow duplication, because you are testing different parts of a structure, e.g. a ring contains an additive and a multiplicative monoid, so you need to test the monoid laws twice.

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.

@TomasMikula

Copy link
Copy Markdown
Contributor

I don't see a way out of having two kind of inheritance relations, short of duplicating laws.

Principled avoids duplication with one kind of inheritance. MonadLaws would inherit FunctorLaws twice, but that would be detected by comparing the two FunctorLaws instances for equality (which reduces to comparing typeclass instances for equality, which is the ugly part of that approach and why I'm not really advocating to use it; though failing to have a meaningful universal equality of typeclass instances would just mean that you get some duplication, but not lose any checks).

One of the problems with discipline is that if I choose a parent, I don't really know which laws I'm inheriting, without studying the hierarchy of the parent (which could change over time without warning). I would therefore resort to just use bases (in order to not miss any checks), but then I have duplicities and thus missed the whole point.

@denisrosset

denisrosset commented Dec 2, 2017

Copy link
Copy Markdown
Contributor

In that case, you lose the possibility of checking both Ring[A].additive: Monoid[A] and Ring[A].multiplicative: Monoid[A]; for this case, you need to use twice the monoid laws.

For that purpose, bases are used when checking an 'embedded structure: for example the semigroups/monoids that appears in lattices, and the monoids/groups that appear in rings/fields. If I were to perform the same checks with Principled, only one of the derived semigroups/monoids would be checked, as the other one would be eliminated by the equality checks.

BTW, I don't think there is any use of bases in cats at the moment.

@TomasMikula

Copy link
Copy Markdown
Contributor

If I were to perform the same checks with Principled, only one of the derived semigroups/monoids would be checked, as the other one would be eliminated by the equality checks.

It wouldn't be eliminated, because the two law sets for monoid would compare as different (because the two Monoid instances would compare as different).

BTW, I don't think there is any use of bases in cats at the moment.

Do cats use discipline much at all?

@denisrosset

Copy link
Copy Markdown
Contributor

@TomasMikula thanks for the clarification!

@denisrosset

denisrosset commented Dec 9, 2017

Copy link
Copy Markdown
Contributor

@johnynek Thanks for intervening. We can indeed remove all abstractions.

We pay a small price, the repetition of the Semigroup. prefix, and of the [A:Arbitrary:Eq] implicits.

I also removed the Rules abstraction, as to provide a single site that documents relevant laws for a typeclass.

Parents are handled by merging Maps, and bases are handled by adding a prefix to the properties of the derived instance (not yet done).

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
      }
    )
}

@TomasMikula

Copy link
Copy Markdown
Contributor

Nice. Maybe don't add the Semigroup. prefix?

@denisrosset

Copy link
Copy Markdown
Contributor

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.

@TomasMikula

Copy link
Copy Markdown
Contributor

The prefix is a convention to avoid name collisions.

I mean, optionally add a prefix when inheriting.

@denisrosset

Copy link
Copy Markdown
Contributor

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.

@TomasMikula

Copy link
Copy Markdown
Contributor

That's a good argument 👍

@LukaJCB

LukaJCB commented Feb 19, 2018

Copy link
Copy Markdown
Member Author

Closing this now :)

@LukaJCB LukaJCB closed this Feb 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants