-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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 ContravariantMonoidal #2034
Add ContravariantMonoidal #2034
Conversation
Adds Divisible typeclass Adds laws for Divisible and tests for relevant instances Adds documentation for Divisible typeclass Adds instances for: - Const - Nested - Tuple2K - Function with Monoid codomain - ReaderT - Kleisli - IdT - Eq - Equiv - Order - Ordering - OptionT - WriterT - IndexedStateT Adds tests
Indeed, for some typeclasses there is no sensible Also, since we don’t use the names |
* Given two values in the Divisible context, and a function producing a value of both types, | ||
* yields an element of the domain of the function lifted into the context. | ||
*/ | ||
def contramap2[A, B, C](fb: F[B], fc: F[C])(f: A => (B, C)): F[A] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not provide a default implementation in terms of contramap
and product
? (like we do for covariant semigroupal: https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/Apply.scala#L44-L50 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, that makes sense, consistency is pretty valuable. I mostly went this approach based on the primary document.
Renames to ContravariantMonoidal Derives contramap2 from product, contramap Moves contramap2 to ContravariantSemigroupal
Renamed to |
@@ -113,14 +113,17 @@ private[cats] trait ComposedContravariantCovariant[F[_], G[_]] extends Contravar | |||
F.contramap(fga)(gb => G.map(gb)(f)) | |||
} | |||
|
|||
private[cats] trait ComposedApplicativeDivisible[F[_], G[_]] extends Divisible[λ[α => F[G[α]]]] { outer => | |||
private[cats] trait ComposedApplicativeContravariantMonoidal[F[_], G[_]] extends ContravariantMonoidal[λ[α => F[G[α]]]] { outer => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn’t repeat Monoidal
here because Applicative
already implies Monoidal
since they are equivalent (see also this detailed explanation: https://stackoverflow.com/questions/23316255/lax-monoidal-functors-with-a-different-monoidal-structure).
Edit: sorry I was wrong. This trait composes a covariant applicative with a contravariant one, so the naming is correct, you can ignore my comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The SO was a good read, anyway, it clarified some of the points on different Monoidal
structures for me, so thank you.
(F.unit).contramap2(fa)(f) <-> fa.contramap(f andThen (_._2)) | ||
|
||
def contravariantMonoidalContramap2DiagonalAssociates[A](m: F[A], n: F[A], o: F[A]): IsEq[F[A]] = | ||
(m.contramap2(n)(delta[A])).contramap2(o)(delta[A]) <-> m.contramap2(n.contramap2(o)(delta[A]))(delta[A]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this law could go to ContravariantSemigroupal
F.map(fa)(G.contramap(_)(f)) | ||
|
||
override def product[A, B](fa: F[G[A]], fb: F[G[B]]) = | ||
F.map2(fa, fb)(G.product(_,_)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a space after the comma here :)
|
||
override def unit[A]: F[G[A]] = F.pure(G.unit) | ||
|
||
override def contramap[A, B](fa: F[G[A]])(f: B => A) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add type annotations here and for product
? :)
(a: A) => f(a) match { | ||
case (b, c) => fb.run(b) && fc.run(c) | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tut fails here, because contramap2
is missing an override modifier and contramap
and product
are unimplemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, thank you. Nice to have tut
catch these things.
@@ -12,4 +12,7 @@ import simulacrum.typeclass | |||
def F = self | |||
def G = Functor[G] | |||
} | |||
|
|||
def contramap2[A, B, C](fb: F[B], fc: F[C])(f: A => (B, C)): F[A] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not exactly sure if we need this definition here, as we already have contramap2
- contramap22
in the auto-generated SemigroupalArityFunctions
. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could mix in the SemigroupalArityFunctions
into the companion object of ContravariantSemigroupal
maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yes! I agree. I didn't realize they were in SemigroupalArityFunctions
. I'll do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left a couple of comments, I think it looks really good already, so thank you very much! Also thanks a lot to @julienrf for reviewing! :)
source: "core/src/main/scala/cats/ContravariantSemigroupal.scala" | ||
scaladoc: "#cats.ContravariantSemigroupal" | ||
--- | ||
# Contravariant Semigroupal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're kind of mixing the Monoidal
and the Semigroupal
in this doc. Maybe we should fix it all to be ContravariantMonoidal
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me, I'll pin it all to ContravariantMonoidal
.
Seems there's still some binary compatibly issues:
If you could add those two to the build.sbt, I think we're done. Thanks again! Edit: |
Codecov Report
@@ Coverage Diff @@
## master #2034 +/- ##
==========================================
- Coverage 95.25% 94.64% -0.61%
==========================================
Files 305 318 +13
Lines 5179 5363 +184
Branches 126 127 +1
==========================================
+ Hits 4933 5076 +143
- Misses 246 287 +41
Continue to review full report at Codecov.
|
@@ -13,3 +13,5 @@ import simulacrum.typeclass | |||
def G = Functor[G] | |||
} | |||
} | |||
object ContravariantSemigroupal extends SemigroupalArityFunctions { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick, but can we get rid of these curly braces? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hah, yeah, for sure.
I wonder, can we create instances for EitherK in the way we can for Applicative? |
```tut:book:silent | ||
import cats.Contravariant | ||
|
||
trait ContravariantMonoidal[F[_]] extends Contravariant[F] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we shouldn't use tut
here as it shadows the real ContravariantMonoidal
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could still use tut
here then add a tut:reset
after (explained here) so that it has no effect after.
Yeah, that seems like a no brainer. I'll do that. Thanks for taking care of the binary compatibility issues. I think I'm not running the check correctly locally, because it keeps telling me it passes 🤔 EDIT: Doesn't look like |
Yeah, you probably need to switch to scala 2.11.11 to see the breakage. |
we squash merge, did you see my comment on using |
Just did, will do 👍 Thanks for the pointer. |
The `ContravariantMonoidal` type class is for [`Contravariant`](contravariant.html) functors that can define a | ||
`product` function and a `unit` function. | ||
|
||
```tut:reset |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I didn't make it clear. I was suggesting using a tut:silent
here and tut:reset:silent
for the next block at line 41.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see, I was just starting to debug this. Ok, cool, sorry about this, I'm not used to testing against multiple versions of the compiler. I'll fix shortly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, checked it against 2.10.6, 2.11.11, and 2.12.3 and the docs all compile with that recommendation. So we should be GTG.
I was sort of negative on this being in cats-core before, but I think it makes a lot of sense especially how we formulated it. It really makes the relationship between Semigroups/Monoids and Semigroupals/Monoidals/Applicatives much clearer for me. So kudos to all! 🎉 |
Awesome! Thanks for the help all involved. Was fun to try my hand at something like this. |
def liftContravariant[A, B](f: A => B): F[B] => F[A] = | ||
ContravariantMonoidal.contramap2(unit[B], _: F[B])(((b: B) => (b, b)) compose f)(self, self) | ||
|
||
// Technically, this is not correct, as the Applicative is composed with the ContravariantMonoidal, not the other way around |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you mind elaborate a bit more (or point to a more detailed discussion/explanation) here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I can also just change the name and move this possibly. The point here is mostly that I didn't want to add anything to Applicative
about ContravariantMonoidal
since this might live in cats.more
, so I ended up having this composition live here in ContravariantMonoidal
even though the composition is Applicative compose ContravariantMonoidal
, so the name and the signature are a little out of sync with what I saw in the rest of the repository.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, I propose we rename and move it to Applicative
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, will do. Thanks.
@@ -165,6 +165,9 @@ private[data] sealed abstract class KleisliInstances1 extends KleisliInstances2 | |||
private[data] sealed abstract class KleisliInstances2 extends KleisliInstances3 { | |||
implicit def catsDataAlternativeForKleisli[F[_], A](implicit F0: Alternative[F]): Alternative[Kleisli[F, A, ?]] = | |||
new KleisliAlternative[F, A] { def F: Alternative[F] = F0 } | |||
|
|||
implicit def catsDataContravariantMonoidalForKleisli[F[_], A](implicit F0: ContravariantMonoidal[F]): ContravariantMonoidal[Kleisli[F, A, ?]] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The convention in cats is that instances that are more specific should be at higher priority. I.E. instance of ContravariantMonoidal
should be lower in the inheritance chain than Contravariant
and ContravriantSemigroupal
and Semigroupal
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha, will fix.
The code looks good for me. But I don't know the area well enough to be comfortable for my sign-off to be the final one for merge. @iravid @julienrf @adelbertc would anyone of you guys give a quick review? (You guys can focus just on the API the laws) |
(F.unit[B], fa).contramapN(f) <-> fa.contramap(f andThen (_._2)) | ||
|
||
def contravariantMonoidalContramap2DiagonalAssociates[A](m: F[A], n: F[A], o: F[A]): IsEq[F[A]] = | ||
((m, n).contramapN(delta[A]), o).contramapN(delta[A]) <-> (m, (n, o).contramapN(delta[A])).contramapN(delta[A]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can’t we move this law to ContravariantSemigroupal
? It seems that we don’t need unit
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, we probably could. It'll just involve adding a ContravariantSemigroupal
laws trait and so forth. Looks like I overlooked your prior comment, sorry about that. I can move that over.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great stuff. Thanks for implementing this @stephen-lazaro.
I left some comments inline; they're mostly around efficiency and consistency.
*/ | ||
def unit[A]: F[A] | ||
|
||
def liftContravariant[A, B](f: A => B): F[B] => F[A] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can actually be moved to Contravariant
, no? (as it's just a curried contramap
)
|
||
def product[A, B](fa: Eq[A], fb: Eq[B]): Eq[(A, B)] = | ||
Eq.instance { (left, right) => fa.eqv(left._1, right._1) && fb.eqv(left._2, right._2) } | ||
Eq.instance { (l, r) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I am not mistaken, the previous version allocates at least one tuple less (although it is admittedly uglier :-))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess that's true, I sort of forgot that aspect of things. 👍
* Note: resulting instances are law-abiding only when the functions used are injective (represent a one-to-one mapping) | ||
*/ | ||
def contramap[A, B](fa: Eq[A])(f: B => A): Eq[B] = | ||
Eq.instance { (l: B, r: B) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason not to delegate to Eq.by, just for consistency?
} | ||
|
||
def product[A, B](fa: Equiv[A], fb: Equiv[B]): Equiv[(A, B)] = | ||
new Equiv[(A, B)] { | ||
def equiv(x: (A, B), y: (A, B)): Boolean = | ||
fa.equiv(x._1, y._1) && fb.equiv(x._2, y._2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment re allocations
val z = fa.compare(x._1, y._1) | ||
if (z == 0) fb.compare(x._2, y._2) else z | ||
} | ||
def compare(x: (A, B), y: (A, B)): Int = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment re allocations
/** Derive an `Order` for `B` given an `Order[A]` and a function `B => A`. | ||
* | ||
* Note: resulting instances are law-abiding only when the functions used are injective (represent a one-to-one mapping) | ||
*/ | ||
def contramap[A, B](fa: Order[A])(f: B => A): Order[B] = Order.by[B, A](f)(fa) | ||
def contramap[A, B](fa: Order[A])(f: B => A): Order[B] = | ||
new Order[B] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same re delegation to Order.by
} | ||
|
||
def contramap[A, B](fa: Ordering[A])(f: B => A): Ordering[B] = | ||
new Ordering[B] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same re delegation to on
val z = fa.compare(x._1, y._1) | ||
if (z == 0) fb.compare(x._2, y._2) else z | ||
def compare(x: (A, B), y: (A, B)): Int = (x, y) match { | ||
case ((aL, bL), (aR, bR)) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same re allocations
Thanks @iravid! I'll take care of those tonight. |
Thanks so much @stephen-lazaro for this awesome contribution! |
🎉 Thanks everyone for the assistance! |
Adds the
Divisible
typeclass as discussed in #1935.I went ahead and opened this against
core
though it was suggested it might belong more inmore
, just because there seems to be thoughts and discussion around #1940 still. This way, we can get something that's relatively ready to merge when/ifmore
is a thing. I'd be happy to move things over at that time.Anyway, some notes:
I switched the names from
divide
andconquer
tocontramap2
andunit
respectively, since that seems a little more in line with the naming standards in cats. Welcome to bike shedding on that front.While implementing, I noticed that the only real contribution
Divisible
gives is the presence of a left and rightunit
along the diagonal, since in point of factcontramap2
is perfectly well implementable onContravariantSemigroupal
as:I could see the impedance of adding another typeclass being high enough that maybe it'd be more reasonable just to add some enrichment to
ContravariantSemigroupal
. I could also see having both, or justDivisible
. Not sure what the most desirable outcome would be here.I did not add an instance for
Show
becauseunit
seems a little against the spirit of the thing, for that particular instance. The only lawful instance AFAICT is the constantly empty stringunit
induced by theA => B
whereB : Monoid
.The branch name is a bit of a misnomer because I decided not to implement
Decideable
cause honestly I had trouble parsing the laws on that typeclass in the primary source here:https://hackage.haskell.org/package/contravariant-1.4/docs/Data-Functor-Contravariant-Divisible.html#t:Divisible
I'll probably take another stab at it unless someone beats me to it.
Added some
tut
documentation, but am happy to drop it entirely if its unwanted.I couldn't figure out how to the higher arity stuff works in
Apply
so I didn't take a shot implementing a dual here. If someone points me in the right direction and it is desired, I'd be happy to try.I added an attribution to
ekmett
in the Scala doc forDivisible
. Hopefully, it satisfies that requirement, but if we have a format I've not met, please do let me know.First time contributing to this repo so I'm sure I missed something. Thanks preemptively for your solicitous review, as well as your forbearance. :)