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 InvariantMonoidal and FreeInvariantMonoidal #845

Merged
merged 4 commits into from
Jun 23, 2016

Conversation

OlivierBlanvillain
Copy link
Contributor

@OlivierBlanvillain OlivierBlanvillain commented Feb 1, 2016

This PR adds InvariantMonoidal and FreeInvariantMonoidal, the invariant analogous to Applicative and FreeApplicative. Following this, we could consider adding a Monoidal type class and explore the Contravariant side of things.

Edit: I'm done with this one, it's good for review (ping @Fristi & @mpilquist)!

I realized that for every instance of InvariantCartesian I had in mind its also possible to define a pure, so I reworked everything to define an InvariantMonoidal and FreeInvariantMonoidal instead of their Cartesian counterpart.

This is work in progress on #802.

Before I finish it (doc is surprisingly long to write 😄), I have concerns regarding the legitimacy of this new InvariantCartesian type class. I could not find new laws for it that cannot be proved by combining laws of Invariant and Cartesian, which makes me think that InvariantCartesian does nothing more than pairing an Invariant with a Cartesian. As such, is it interesting at all? I'm pretty sure that the FreeInvariant stuff (the original motivation for this PR), could be formulated directly in term of Invariant and Cartesian.

Are there other example of type classes already in cats that add nothing more than "hey, I'm this one plus this one, that it!"? If we look at Apply as an example, is there a thing which has a Functor and a Cartesian, but not an Apply? If the answer is no, then why do we need Apply at all, given that all code using it could be rewritten in term of Functor and Cartesian?

I'm a bit confused, not really sure if my questions/concerns make sense here...

@codecov-io
Copy link

codecov-io commented Feb 1, 2016

Current coverage is 88.79%

Merging #845 into master will increase coverage by 0.01%

  1. 4 files (not in diff) in ...cats/laws/discipline were modified. more
    • Hits +24
  2. 3 files (not in diff) in ...main/scala/cats/laws were modified. more
  3. 5 files (not in diff) in ...in/scala/cats/syntax were modified. more
    • Misses +8
    • Hits -13
  4. 8 files (not in diff) in .../main/scala/cats/std were modified. more
    • Misses +11
    • Hits +40
  5. 8 files (not in diff) in ...main/scala/cats/data were modified. more
    • Misses +6
    • Hits -29
  6. 10 files (not in diff) in .../src/main/scala/cats were modified. more
    • Misses +40
    • Hits -36
  7. 4 files (not in diff) in ...main/scala/cats/free were created. more
  8. 2 files (not in diff) in ...main/scala/cats/data were created. more
  9. 1 files (not in diff) in .../src/main/scala/cats were created. more
  10. 4 files in ...main/scala/cats/data were modified. more
    • Hits -15
@@             master       #845   diff @@
==========================================
  Files           228        232     +4   
  Lines          3012       3069    +57   
  Methods        2962       3017    +55   
  Messages          0          0          
  Branches         47         49     +2   
==========================================
+ Hits           2674       2725    +51   
- Misses          338        344     +6   
  Partials          0          0          

Sunburst

Powered by Codecov. Last updated by 400c24d...5d4170b

@julienrf
Copy link
Contributor

julienrf commented Feb 1, 2016

Interesting. I’m not sure FreeInvariant is a good name, though. What you actually have is a “free invariant cartesian functor”.

Also, are we going to have a FreeXxx for each combination of (Functor + Invariant + Contravariant) × (Monad + Cartesian + Monoidal)?

@OlivierBlanvillain
Copy link
Contributor Author

Also, are we going to have a FreeXxx for each combination of (Functor + Invariant + Contravariant) × (Monad + Cartesian + Monoidal)?

@julienrf I think we are almost there. Unfolding your ×, these are the practical cases:

  • FreeFunctorMonad → That's cats.free.Free.
  • FreeFunctorMonoidal → That's cats.free.FreeApplicative.
  • FreeContravariantMonoidal → Probably interesting for pretty printers, to be investigated. (Would this be FreeDivisible?)
  • FreeInvariantMonoidal → That's the cats.free.InvariantMonoidal I would like to contribute :)

I think there would be no use cases of the other ones:

  • FreeContravariantMonad → I don't think a ContravariantMonad would make sense.
  • FreeInvariantMonad → Same.
  • FreeFunctorCartesian → This would be a FreeApply, but if for every Apply instance we also have an Applicative, it would be useless.
  • FreeInvariantCartesian → Doable but also useless if for every InvariantCartesian instance we also have an InvariantMonoidal.
  • FreeContravariantCartesian → Same.

That being said ìt might be possible to factor out the implementation of FreeApplicative and FreeInvariantMonoidal (and FreeDivisible?) into a more abstract structure. The related work of Free Applicative Functors by Paolo Capriotti mentions the HFree package in Haskell as some sort of "higher order free".

@OlivierBlanvillain OlivierBlanvillain changed the title Add InvariantCartesian and FreeInvariant Add InvariantMonoidal and FreeInvariantMonoidal Feb 15, 2016
@@ -16,4 +16,13 @@ import simulacrum.typeclass
def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
}

object Cartesian extends CartesianArityFunctions
object Cartesian extends CartesianArityFunctions with AlgebrCartesianInstances
Copy link
Contributor

Choose a reason for hiding this comment

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

s/AlgebrCartesian/AlgebraCartesian?

@adelbertc
Copy link
Contributor

Hey @OlivierBlanvillain - sorry this took so long to review.

I have similar concerns to Julien regarding having a bunch of Free variants - I'm struggling because I can justify FreeApplicative and FreeMonad since Applicative and Monad capture fundamental properties of evaluation (e.g. context free vs. context dependent) though I also don't know where Cats should draw the line on including such structures. Including something like FreeInvariantX would tempt me to want to have the others if only for consistency's sake.. but then again I have an obsession with consistency.

Code-wise everything looks good modulo a couple typos and questions, but I'd like to hear other people's thoughts as well. @ceedubs ?

@OlivierBlanvillain
Copy link
Contributor Author

I would argue that Applicatives are only one part of the story as far as capturing fundamental properties of context free evaluations goes. If Divisible & FreeDivisible where also included it would lead to a nice symmetry between the available "composition syntaxes" and the Free structures:

Algebraic Beast Syntax Free version Free use case
Monad for {} yield f(...) Free Monad Tech talks 😄
Functor `(x @ y).map(f)`
Contravariant `(x @ y).contramap(f)`
Invariant `(x @ y).imap(f)(g)`

@ceedubs
Copy link
Contributor

ceedubs commented Apr 19, 2016

Warning: rambling brain dump below.

@OlivierBlanvillain thank you for this PR and like @adelbertc, I have to apologize for the radio silence for so long. Your code is high quality, but as mentioned it's tough to make decisions on what to include in cats and what not to. Some people have already complained about the cats-core jar being pretty large. I think that covariant forms of free (free monad, free applicative) are getting most of the attention in the FP Scala community right now, but I agree with you that the contravariant and invariant forms can also be really useful.

This puts my brain into a spiral of "So do we include this? Do we break free structures into a separate module/library? Do we have different modules for different forms of free structures?". At the moment I think going back to having free in a separate module is most appealing to me, but we've already been there and back, so I'm afraid that might frustrate people. If I recall, one of the biggest reasons that we broke free into a separate module was for trampolining, which is now handled by Eval. Personally I'm not too bothered by a large jar size, but I know that there are people who for various reasons are.

That's a partial braindump of my thoughts. Clearly there are no answers in there. I think I'm pretty happy with any route forward from here. Would people hate me if I put together a proof of concept PR of separating out a module or two now that the cats landscape has shifted a bit thanks to Eval and some other things?

@ceedubs
Copy link
Contributor

ceedubs commented May 19, 2016

Sorry for not responding for so long but I started pondering a project yesterday that I think could really benefit from this. Now that free has its own module again, I'm less concerned about adding more structures to it.

The next week is really busy for me, but I'm hoping to do a more thorough review of this in the near future. I'm also open to any naming ideas that people have.

@OlivierBlanvillain
Copy link
Contributor Author

Cool :). I played a bit more with it (still in the context of codecs) and I have a cool example where the same "configuration" is used to generate a json parser, a json pretty printer, and a json schema. This example has quite a bit of boilerplate / manual wiring to combine unrelated algebras, but I have the feeling that this approach could lead to a neat alternative to automatic type class derivation entirely with cats.

def combine(x: B, y: B): B = f(fa.combine(g(x), g(y)))
}

def pure[A](a: A): Semigroup[A] = new Semigroup[A] {
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems a little strange. Does it make sense to have this?

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 don't think it's wrong, but I'm not it's useful...

@ceedubs
Copy link
Contributor

ceedubs commented May 21, 2016

Should Applicative now extend InvariantMonoidal?

@julienrf
Copy link
Contributor

Should Applicative now extend InvariantMonoidal?

Yes, I think so.

@julienrf
Copy link
Contributor

julienrf commented May 21, 2016

It would be great to also have Monoidal on its own, which would extend Cartesian and add pure[A](a: A): F[A].

@OlivierBlanvillain do you really need pure or would unit: F[Unit] be sufficient? (see also the discussion in #817)

@OlivierBlanvillain
Copy link
Contributor Author

Should Applicative now extend InvariantMonoidal?

Won't this lead to a weird diamond inheritance with Invariant being on both sides? I tried it but could not make scalac/simulacrum happy :/ OlivierBlanvillain@6226b5c

It would be great to also have Monoidal

Shall I do it in the same PR?

@OlivierBlanvillain do you really need pure or would unit: F[Unit] be sufficient? (see also the discussion in #817)

def unit: F[Unit] is sufficient since def pure[A](x: A): F[A] = imap(unit)(_ => x)(_ => ()). But since they are equivalent, I feel that pure is more elegant in that case.

@julienrf
Copy link
Contributor

julienrf commented May 24, 2016

It would be great to also have Monoidal

Shall I do it in the same PR?

I think we could it afterward, let’s progress iteratively!

def unit: F[Unit] is sufficient since def pure[A](x: A): F[A] = imap(unit)(_ => x)(_ => ()). But since they are equivalent, I feel that pure is more elegant in that case.

They are indeed equivalent once you add functor capabilities to your F[?] (which is eventually always the case, I think…). So maybe it is OK to keep pure… Do @mpilquist or @travisbrown have an opinion on that?

@Fristi
Copy link
Contributor

Fristi commented Jun 11, 2016

So these codecs (which are encode/decode really) are also encoded in Haskell as partial isomorphisms. See a example on this:

Bidirectional Parsing - https://youtu.be/NHRIV7UNiPU?t=41m24s

He points us to

What we are trying to achieve with FreeInvariantMonoidal is something similar to the partial isomorphisms. I am considering to write my routing lib using, partial isos, what do you think @OlivierBlanvillain. Would this be applicable for your lib aswell?

@OlivierBlanvillain
Copy link
Contributor Author

OlivierBlanvillain commented Jun 11, 2016

Hey @Fristi :) I gave a superficial read to Invertible Syntax Descriptions a while ago, and indeed, they are trying to achieve the same thing WRT encoders/decoders. However, I think that the FreeInvariantMonoidal is more general that what is done in this paper, my very fuzzy memory tells me that their approach would not be appropriate to generate something like a documentation type class (along with an encoder/decoder), while FreeInvariantMonoidal let's you do that.

@Fristi
Copy link
Contributor

Fristi commented Jun 12, 2016

Hmm you are right. Introspection is not included. A partial iso is just two functions. To make a category of it you should use kleisli arrows to make them compose. Monoids are used to make choice between partial isos possible.

I've skimmed through JsonGrammer2 sources and it appears he using a free category (see: https://github.com/MedeaMelana/JsonGrammar2/blob/master/src/Language/JsonGrammar/Grammar.hs#L40). As you can see he also lifts; category and monoid operations to the algebra. After that he just makes instances category and monoid instances for his algebra. This is exactly what we are doing at the moment. We also lift some operations of Monoidal and Invariant to a 'generic' and free algebra, but it has no real strong name (and that's something which itches for me ;p).

He also uses stack prisms (which I haven't figured out yet, but it seems like some sort of heterogenous data structure to carry product type information). Monoids could be used to encode coproduct types in to these codecs I think.

As a side note, something else I found: https://ocharles.org.uk/blog/posts/2014-06-10-reversible-serialization.html. Ollie Charles also did some work on this, have to read it though :)

@ceedubs
Copy link
Contributor

ceedubs commented Jun 14, 2016

@Fristi @OlivierBlanvillain do you have a consensus on what the right way to go forward is? It seems like there's a lot of great stuff in this PR, so I don't want to hold it up forever, but if you have better ideas brewing, then we can of course put it on the back burner.

@OlivierBlanvillain
Copy link
Contributor Author

@ceedubs I would say that the InvariantMonoidal and FreeInvariantMonoidal proposed in the PR are simply the invariant analogous to Applicative and FreeApplicative. That's definitely a niche, but it's also unlikely to change. I would not be concerned about future work undoing what's is done here.

This is how I see things going forward:

  • Merge/close this (depending on whether or not this stuff could be useful for someone else)
  • Work on Monoidal
  • Explore the Contravariant side of things

@ceedubs
Copy link
Contributor

ceedubs commented Jun 14, 2016

@OlivierBlanvillain gotcha. That sounds good to me, and @mpilquist expressed interest in #802, so I suspect that he's on board.

It looks like there are merge conflicts again. Could you please refrain from making unrelated formatting changes in future PRs -- it can lead to lots of merge conflicts and makes the diffs a little tougher to review. It'd be great if you could also adopt the new naming convention for implicit instances (#1061), but that can wait until a subsequent PR if you'd prefer.

@Fristi
Copy link
Contributor

Fristi commented Jun 14, 2016

@ceedubs @OlivierBlanvillain Sounds fine by me! Let's keep this train going 👍

@OlivierBlanvillain OlivierBlanvillain force-pushed the free-invariant branch 3 times, most recently from 9ecdb34 to 01f82dd Compare June 14, 2016 20:56
@OlivierBlanvillain
Copy link
Contributor Author

I resolve the conflicts. All the formatting stuff is isolated in the first commit (which I rebased with a brutal git checkout --ours . :P), sorry if it interfered with the review...

}
```

Given an instance of `InvariantMonoidal` for `Semigroup`, we are able to combine existing `Semigroup` instances to form a new `Semigroup` by using the `Catesian` syntax:
Copy link
Member

@DavidGregory084 DavidGregory084 Jun 14, 2016

Choose a reason for hiding this comment

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

Catesian -> Cartesian

@@ -83,4 +83,12 @@ object eq {
implicit val unitEq: Eq[Unit] = new Eq[Unit] {
def eqv(a: Unit, b: Unit): Boolean = true
}

// To be removed once https://github.com/non/algebra/pull/125 is published
implicit class EqAnd[A](self: Eq[A]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This shouldn't be needed any more.

*/
private[cats] sealed trait KernelCartesianInstances {
implicit val catsInvariantSemigroup: Cartesian[Semigroup] = InvariantMonoidal.catsInvariantMonoidalSemigroup
implicit val catsInvariantMonoid: Cartesian[Monoid] = InvariantMonoidal.catsInvariantMonoidalMonoid
Copy link
Contributor

Choose a reason for hiding this comment

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

Since InvariantMonoidal extends Cartesian, can these just live in the InvariantMonoidal companion object without also living here?

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 catch, indeed it looks like scalac is able to find the ones in InvariantMonoidal companion object!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually I take that back, the tut compilation fails because it can't find Cartesian[Semigroup] with this change. I reverted it and made this commit for reference: OlivierBlanvillain@07d2c14

- Rename Algebra*Instances → Kernel*Instances
- Rename implicit instances
- Typo in doc
- Remove `implicit class EqAnd` (not needed any more)
@ceedubs
Copy link
Contributor

ceedubs commented Jun 18, 2016

I have one minor comment about an outdated reference to Algebra, but other than that, this looks good to me. @mpilquist or @adelbertc wdyt?

@kailuowang
Copy link
Contributor

👍 It looks by and large good to me. Given the size of the PR, it certainly has space for further improvements (e.g. documentation on the free side), but I think those non-urgent improvements can come later. Would love to see people start profiting from this sooner rather than later.

@ceedubs
Copy link
Contributor

ceedubs commented Jun 23, 2016

🎉

@ceedubs ceedubs merged commit c869d7b into typelevel:master Jun 23, 2016
@Fristi
Copy link
Contributor

Fristi commented Jun 23, 2016

Nice work @OlivierBlanvillain 👍 🎉

@OlivierBlanvillain OlivierBlanvillain deleted the free-invariant branch June 23, 2016 07:48
@OlivierBlanvillain
Copy link
Contributor Author

Thanks all for your help! 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants