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

Add Decidable #2607

Open
wants to merge 91 commits into
base: main
Choose a base branch
from

Conversation

stephen-lazaro
Copy link
Contributor

@stephen-lazaro stephen-lazaro commented Nov 12, 2018

Resolves #1935 and #2604.

Seeing an issue with binary compatibility I'm not sure how to resolve correctly.
Maybe someone can help me out? I'm probably misunderstanding the intended function of the BinCompat traits.

Included a very barebones doc page.

Since the laws are not formalized in ekmett's original, I've done a bit of guesswork / intiuitioneering on what the right laws look like. Open to discussion.

build.sbt Outdated Show resolved Hide resolved
@stephen-lazaro stephen-lazaro changed the title Slazaro/decideable Add Decideable Nov 12, 2018
def chosen[B, C](fb: F[B], fc: F[C]): F[Either[B, C]] =
decide(fb, fc)(identity[Either[B, C]])

def zero: F[Nothing] = trivial[Nothing]
Copy link
Member

Choose a reason for hiding this comment

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

Are these two always the same? I feel like it's a bit weird that both the additive and multiplicative identities are the same, but it might very well be that my intuition for these sorts of things are off 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am a bit suspicious of it as well... For example, the case of maps into Semirings that doesn't quite feel like the right choice... It might bear more thinking on.

Copy link
Member

@LukaJCB LukaJCB Nov 13, 2018

Choose a reason for hiding this comment

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

Yeah, I think we might need to change this. I think for any [A, B: Semiring]: A => B you'll want unit to be A => 1 and zero to be A => 0. With this default, zero would be the same as unit, which we don't want.
I think we can already see this in this PR, where you explicitly override zero in the Predicate instance (which can be seen as a specialization of [A, B: Semiring]: A => B) in order for it to be const(false) instead of const(true) :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that was sort of my intuition as well. I'm a bit puzzled at the original's choice to use Void but, there's probably some reason. Regardless, will change.

Copy link
Contributor

Choose a reason for hiding this comment

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

See my comment in #2620 (comment)

trait DecideableLaws[F[_]] extends ContravariantMonoidalLaws[F] {
implicit override def F: Decideable[F]

def decideableDecideRightAbsorption[A](fa: F[A]): IsEq[F[A]] =
Copy link
Member

Choose a reason for hiding this comment

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

This one tests right identity, not absorption, right?
Absorption would be product(fa, zero) == zero

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call, that' definitely the right name. Morally, I think then we're trading an identity for the traditional absorption on Alternative which makes sense. It's hard to imagine an absorption law for sum or decide that would make sense.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, I'm not sure I agree, in Alternative we have both identity and absorption, I think we could have both for Decidable as well. :)

Copy link
Member

Choose a reason for hiding this comment

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

For ContravariantMonoidal we already have left and right multiplicative identity as well as associativity.
So for Decidable I think we need :

  1. sum and zero Left Identity
  2. sum and zero Right Identity
  3. sum Associativity
  4. product and zero Right absorption
  5. sum and contramap Right Distributivity
  6. sum and product Right Distributivity

And then we should be good IMO :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah I see how you'd write absorption I think now, yeah, sounds good.

Copy link
Contributor Author

@stephen-lazaro stephen-lazaro Jul 23, 2020

Choose a reason for hiding this comment

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

Interestingly, having both left and right identities seems to rule out OptionT because you have to make a choice for None (either a Left or a Right, which contradicts one or the other identity law, at least if I've written them in a reasonable way), and similarly the sum and product distribution rules out Order and Ordering instances as you end up reversing orderings in the course of that, though I'm working to see if I can fix those instances to respect it.

Copy link
Member

@LukaJCB LukaJCB left a comment

Choose a reason for hiding this comment

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

Massive thanks @stephen-lazaro! Really great work on this PR. I've added a bunch of comments, but none are really major I think :)

@LukaJCB LukaJCB self-requested a review November 13, 2018 08:43

In other words, if we have two contexts `F[A]` and `F[B]` and we have a procedure `C => Either[A, B]` that could produce either an A or a B depending on the input, we have a way to produce a context `F[C]`.

The classic example of a `Decideable` is that of predicates `A => Boolean`, leaning on the fact that both `&&` and `^` are valid monoids on `Boolean`.
Copy link
Member

Choose a reason for hiding this comment

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

Another note on this one, the default Rig for Boolean uses || instead of ^, should we use || here as well? https://github.com/typelevel/algebra/blob/master/core/src/main/scala/algebra/instances/boolean.scala

@stephen-lazaro
Copy link
Contributor Author

Thanks for the feedback @LukaJCB ! It may take me a day or two to incorporate, but good points all.

Copy link
Member

@armanbilge armanbilge left a comment

Choose a reason for hiding this comment

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

Thanks for getting through my previous review, now I'll try and turn my attention to the laws 😅

package cats
package laws

trait DecidableLaws[F[_]] extends ContravariantMonoidalLaws[F] {
Copy link
Member

Choose a reason for hiding this comment

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

So a couple concerns about the laws (and again, apologies in advance for any hare-brained comments :)

  1. they don't seem to invoke zero or lose anywhere, so their implementations are not actually checked.
  2. usually when providing default implementations for methods (decide and lose here) we add a simple law to check the actual implementation matches the default. This would fix (1) and also makes me wonder if strictly speaking we need the decide identity laws, although I guess they don't hurt :)

Copy link
Contributor Author

@stephen-lazaro stephen-lazaro Jul 8, 2022

Choose a reason for hiding this comment

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

I can add some on those certainly, I think keeping the identity laws is a good idea though because they nicely capture some of the vaguely monoid-like character of this stuff.

@armanbilge
Copy link
Member

@stephen-lazaro
Copy link
Contributor Author

stephen-lazaro commented Jul 8, 2022

Yeah, I'd read that Kmett thread back when opening this - it's true that it's a bit difficult to say what exactly the right choices are. I think we vaguely our choices here largely on the principle of lifting Rig laws when we were doing this a few years ago (since this is sort of a contravariant categorification of a Rig if I'm remembering things well).
Luka also suggested some options here IIRC: #2607 (comment)

@stephen-lazaro
Copy link
Contributor Author

stephen-lazaro commented Jul 12, 2022

It gets pretty difficult to actually run some of the laws even once they are expressed, because generating things with Nothing in their type gets pretty hard, and much worse when the law can be run but there is no view to Prop from IsEq[F[Nothing]].
E.g. this candidate for right absorption (the best I can come up with for what that ought to look like):
F.contramap(F.product(fa, F.zero))((n: Nothing) => (n: A, n)) <-> F.zero
or this candidate for testing lose:
F.lose(f) <-> F.contramap(F.zero)(f)
EDIT: I've added the tests anyway, I had to sort of rig the instances but it's hard to imagine what a meaningful instance would be in these cases anyway (e.g. Eq[F[Nothing]]? strange concept).

@stephen-lazaro
Copy link
Contributor Author

@armanbilge This is probably as complete as it's going to get at this stage - let me know if there are thoughts on it.

@armanbilge
Copy link
Member

Ah, great, thanks for the bump and all your work! I will try to take a look this weekend.

Copy link
Member

@armanbilge armanbilge left a comment

Choose a reason for hiding this comment

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

Sorry that it took me forever to cycle back, had to work up the courage to face this PR again 😅

Just a couple comments for now, but will slowly be combing through this. Really want to land this in the next Cats release.

Also can we add a Decidable[Show] instance? Nopeee 😂 ignore my inane comment.

Comment on lines +48 to +53
def sum[A, B](fa: Eq[A], fb: Eq[B]): Eq[Either[A, B]] =
Eq.instance {
case (Left(a1), Left(a2)) => fa.eqv(a1, a2)
case (Right(b1), Right(b2)) => fb.eqv(b1, b2)
case _ => false
}
Copy link
Member

Choose a reason for hiding this comment

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

This is equivalent to the usual Eq[Either[A, B]] right? We can probably just delegate to that instances.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call! Thank you.

Comment on lines +36 to +44
trait Decidable[F[_]] extends ContravariantMonoidal[F] {
def sum[A, B](fa: F[A], fb: F[B]): F[Either[A, B]]
def zero: F[Nothing]

def decide[A, B, C](fa: F[A], fb: F[B])(f: C => Either[A, B]): F[C] =
contramap(sum(fa, fb))(f)

def lose[A](f: A => Nothing): F[A] = contramap[Nothing, A](zero)(f)
}
Copy link
Member

Choose a reason for hiding this comment

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

Ok I realize this is a bit late in the day, but wouldn't it make more sense to define it like this? Basically trade two abstract defs (contramap+sum) for one (decide).

// abstract
def decide[A, B, C](fa: F[A], fb: F[B])(f: C => Either[A, B]): F[C]

// concrete
def contramap[A, B](fa: F[A])(f: B => A): F[B] =
  decide(fa, zero)(b => Left(f(b)))
def sum[A, B](fa: F[A], fb: F[B]): F[Either[A, B]] =
  decide(fa, fb)(identity)

Copy link
Member

@armanbilge armanbilge Oct 12, 2022

Choose a reason for hiding this comment

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

Based on principle of power, I suspect that this similar adjustment should also be made.

// abstract
def lose[A](f: A => Nothing): F[A]

// concrete
def zero: F[Nothing] = lose(identity)

Copy link
Contributor Author

@stephen-lazaro stephen-lazaro Oct 28, 2022

Choose a reason for hiding this comment

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

I'm fine with this! I think this actually better fits our current modeling with a monoidal / semigroupal hierarchy. This encoding I think predates some of the rest of that work and was originally part of that push.
EDIT: Remembering now, I think actually we went back and forth on this and at that time sentiment went otherwise, I am flexible on this - I think either makes sense but perhaps this is more coherent with the traditional modeling of say implementing Apply. At one point, I think interest went more towards having the monoidal, comonoidal, or unit items be the fundamentals, and everything else be derived (i.e. not principle of power, but principle of algebraic composition, as it were).

@armanbilge
Copy link
Member

Does anyone else have thoughts on my latest comments? Specifically #2607 (comment). I am happy to try and push this across the finish line if Stephen is busy atm.

This is phenomenal work, and I'd really like to land it before another 4 years pass 😄

@stephen-lazaro
Copy link
Contributor Author

stephen-lazaro commented Oct 28, 2022

Apologies! I have been preoccupied with my wedding. Seeing that there are comments here, I will endeavor to resolve them this weekend. If there is other feedback, I would appreciate having that up front so I can do them all in a few iterations, that said if it takes a few over that I understand - we all have constraints.

@armanbilge armanbilge modified the milestones: 2.9.0, 2.10.0 Oct 30, 2022
@armanbilge armanbilge modified the milestones: 2.10.0, 2.11.0 Aug 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Divisible and Decideable (contravariant Applicative and co.)
8 participants