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

Created Copair typeclass #976

Closed
wants to merge 18 commits into from
Closed

Created Copair typeclass #976

wants to merge 18 commits into from

Conversation

Jacoby6000
Copy link
Contributor

@Jacoby6000 Jacoby6000 commented Apr 12, 2016

This is a PR attempting to take care of #974. I'm mostly looking for feedback right now, as I'm new to FP, and contributing to Cats.

A Copair is a generalization of Xor, Either, and Validated.

What's missing currently are tests, mainly because I'm not sure what laws Copair should have. I've thought of f.swap.swap <-> f but that's about it. However I know there are more. These things just aren't intuitive to me yet.

I'm also looking for feedback on how to organize things. As it stands now, there will be problems with ambiguous implicits, because of the Bitraverse derivation. I can move it somewhere to make this better, but I'm not sure where it should go. I'm also not sure where/how I should handle the CopairIdOps class.

@adelbertc
Copy link
Contributor

Ideas for some laws:

left(a).fold(f, g) == f(g)
right(b).fold(f, g) == g(b)

// left is associative
left(left(x, y), z) ~= left(x, left(y, z))

// similarly for right

It can also extend/implement Bitraverse (and by extension Bifoldable and Bifunctor).

I have had a use case for this once I think, but I don't see this around much so I'm wondering if there's a reason for that..

Assuming there's no reason, there is possibly an analogous one abstracting over products. In general this design looks a lot like MTL/finally tagless (e.g. ApplicativeError, MonadReader, etc.)

/cc @tpolecat since I saw him briefly discussing this with you on Gitter

@Jacoby6000
Copy link
Contributor Author

I had initially created this with an implicit function that would derive a Bitraverse from a Copair, but I've updated Copair to extend Bitraverse. I also went ahead and replaced Either/Xor/Validated's Bitraverse instances with Copair instances.

@codecov-io
Copy link

codecov-io commented Apr 13, 2016

Current coverage is 82.37%

Merging #976 into master will increase coverage by +0.37%

  1. 2 files in ...main/scala/cats/data were modified. more
    • Misses -1
    • Hits +1
  2. 3 files (not in diff) in ...main/scala/cats/laws were modified. more
  3. 2 files (not in diff) in ...n/scala/cats/functor were modified. more
  4. 6 files (not in diff) in .../src/main/scala/cats were modified. more
    • Misses -2
    • Hits +2
@@             master       #976   diff @@
==========================================
  Files           215        219     +4   
  Lines          2704       2745    +41   
  Methods        2639       2681    +42   
  Messages          0          0          
  Branches         60         60          
==========================================
+ Hits           2217       2261    +44   
+ Misses          487        484     -3   
  Partials          0          0          

Powered by Codecov. Last updated by 8355a19...33f16d9

@Jacoby6000
Copy link
Contributor Author

@adelbertc I'm not sure what you mean by

left(left(x, y), z) ~= left(x, left(y, z))

left and right only take a single parameter.

@adelbertc
Copy link
Contributor

Ah yes my mistake, ignore that one

@adelbertc
Copy link
Contributor

I wonder if you can also define an instance for Coproduct.

This LGTM, I've had a use case for this before I think, though I have (purely aesthetic) reservations about the name Copair.

@Jacoby6000
Copy link
Contributor Author

I'm not married to the name Copair, it's just what @tpolecat recommended. There's still stuff missing here though. If you squint at it, you can derive a Monad from Copair. The problem with doing that, is that Validated would not behave properly as a Monad, so I'm not sure that's a good idea... but the possibility exists.. I've also not yet defined the typeclasses for checking equality.

Another question, how to I get code coverage on the *Ops classes? I followed the standard the other laws were following, but they seem to test the typeclass directly, and miss the *Ops classes.

@tpolecat
Copy link
Member

I just made up Copair, I have no strong opinion.

@Jacoby6000
Copy link
Contributor Author

@adelbertc Looks like Haskell has a Copair and it (from what I can tell) is different. So a different name should probably be here

Upon further inspection, maybe Haskell's Copair is the same. Haskell's Either does not have a Copair instance though. I'm not a haskeller, so I dunno what that means here.

@Jacoby6000
Copy link
Contributor Author

@adelbertc after my (Short) investigation, it looks like you cannot define an instance for Coproduct.

Whenever I tried to define fold (F[A,B] => (A => C, B => C) => C), I began to have trouble.

I've added a to[G[_,_]: Copair] function for converting between Copairs. I'm not sure what else can be added, but there very well may be more.

I'm confident with Copair where it sits now, but please feel free to suggest a new name, also please reference my link to hackage above, because I can't read Haskell. If Haskell's Copair is different from this, then we should not name it Copair.

@Jacoby6000
Copy link
Contributor Author

Also something I just noticed, Validated appears to only have an instance for Bifunctor. Adapting Validated to use Copair would mean that Validated will now have a Bitraverse instance. Is this a problem?

Seen here

Conflicts:
	core/src/main/scala/cats/data/Validated.scala
	core/src/main/scala/cats/data/Xor.scala
	tests/src/test/scala/cats/tests/EitherTests.scala
	tests/src/test/scala/cats/tests/ValidatedTests.scala
	tests/src/test/scala/cats/tests/XorTests.scala
@Jacoby6000
Copy link
Contributor Author

Updated branch and resolved conflicts

@Jacoby6000
Copy link
Contributor Author

I don't quite understand the codecov/changes error. Can I have some guidance on how to fix that?

@ceedubs
Copy link
Contributor

ceedubs commented May 14, 2016

Thanks for putting this together @Jacoby6000.

I'm not sure what to think about this. Since Xor and Either are effectively the same thing, I don't know that I'd put a lot of value in an abstraction over the two of them - you'd just be encoding a specific data structure as a type class. The fact that this abstraction also covers Validated makes it much more interesting to me. However, it's troublesome to me that (as @Jacoby6000 has mentioned) Copair could extend Monad without adding any abstract methods, and that would result in a lawful Monad for Xor but not for Validated. To me that makes it seem like something is off here.

@Jacoby6000
Copy link
Contributor Author

It may be that this construct doesnt belong in cats, but maybe something that extends cats in some way. Sometimes when developing libs, it's useful to accept/manipulate whatever Either construct somebody wants to use. Perhaps I can look at throwing this in some utility library. Perhaps it'd be a nice addition to Daniel's "Shims"

@kcsongor
Copy link
Contributor

It looks to me like this type-class captures the notion of the sum of two types A and B (i.e. anything that is isomorphic to Either[A, B], with A and B not necessarily parametric, see Option as an example below).

As @ceedubs said, Xor and Either are effectively the same thing, but I would argue that so is Validated - at least at the algebraic level. The data structures are all the same, the difference lies in the specific type-class instances of these structures, and in fact, this is the reason why multiple versions of the same data structure exist. (by 'data-structure', I mean the sum referred to above)

So your Copair (which I would call Sum, by the way) captures this specific algebraic structure. Now, Option[A]'s algebraic structure is isomorphic to Either[(), A], and so it could also be made a Copair, with a type lambda that discards one of the arguments. (<- I'm not sure how this would play, I'm very new to scala)

Usually, the notion of abstracting over algebraic properties of ADTs (algebraic data types) is called 'generic' programming, and that is what the shapeless library is concerned with (https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/ops/coproduct.scala for your specific use-case)
I think this is a good example of using generic programming to derive type-class instances, which is in turn what the kittens library does (using shapeless).

@Jacoby6000
Copy link
Contributor Author

Jacoby6000 commented Sep 8, 2016

@kcsongor I opened a PR in the kittens repo. I feel like Sum is too general for this type. A Sum can usually contain an arbitrary number of members, shapeless.Coproduct might be more of a Sum than this. In haskell there appears to be a Copair, which from what I can tell is identical to this.

@kcsongor
Copy link
Contributor

kcsongor commented Sep 8, 2016

Hm, what I meant was that kittens already has this abstraction, as this describes a binary sum of types. Of course, if you know how to represent the sum of two types, then you know how to represent the sum of n types, neither is more general. Either[A, Either[B, Either[C, D]]] is a long-winded way of writing the sum of 4 types. You might have to squint a bit to see how shapeless's coproduct is talking about the same thing, as it involves some extra machinery to support a more convenient type-level representation of the sums.

This is irrelevant, but: the link you posted earlier for Haskell is not quite the same thing. The docs say that Copair is dual to Unpair, where if Unpair eliminates a pair in a context, then Copair introduces one. The example states that this is useful for contravariant functors. That's because flipping the arrows of the product projections via some contravariant functor f, from (a, b) -> a and (a, b) -> b, you get exactly f a -> f (a, b) and f b -> f (a, b) respectively. Of course there are some similarities, because at the end of the day, all sums are created equal!

@Jacoby6000
Copy link
Contributor Author

@kcsongor I'm aware of what coproducts are and do. In short, my vision of this Copair is a generalization to unify all coproducts with an arity of 2, because there are so many different ones in existence.. Validated and Xor are both coproducts, but they have different effects. The typeclass I've created allows you to accept either one of those, provided you don't want behaviors specific to those two specific coproducts for example.

The fact that this is a generalization of all coproducts with arity 2 might be seen more clearly here:

  type ShapelessCoproduct2[A, B] = A :+: B :+: CNil
  implicit val shapelessCoproductCopair: Copair[ShapelessCoproduct2] =
    new Copair[ShapelessCoproduct2] {
      def fold[A, B, C](f: CopairInstances.this.ShapelessCoproduct2[A,B])(fa: A => C,fb: B => C): C =
        f match {
          case Inl(a) => fa(a)
          case Inr(Inl(b)) => fb(b)
          case Inr(Inr(_)) => sys.error("impossible")
        }

      def left[A, B](a: A): CopairInstances.this.ShapelessCoproduct2[A,B] = Coproduct[ShapelessCoproduct2[A,B]](a)
      def right[A, B](b: B): CopairInstances.this.ShapelessCoproduct2[A,B] = Coproduct[ShapelessCoproduct2[A,B]](b)
     }

@kcsongor
Copy link
Contributor

kcsongor commented Sep 8, 2016

"The typeclass I've created allows you to accept either one of those, provided you don't want behaviors specific to those two specific coproducts for example." - this is what data-type generics programming is for. Shapeless is able to derive this binary coproduct for both Either and Xor, I suggest you to look up how the library works, quite interesting!
Your instance is not a proof that copair is more general, that would only be the case if you couldn't achieve with coproduct what you can with copair, but I think you can in all cases.

@Jacoby6000
Copy link
Contributor Author

I'm very familiar with shapeless, and I'm not at all arguing that my Copair is more general. My Copair is more specific. And because it's more specific, you can leverage things that you get because you have an Arity of 2, like bitraverse and bifunctor. But then also left[A](a: A) and right[B](b: B) which allow you to create "lefts" and "rights", which bifunctor and bitraverse do not give you.

@kcsongor
Copy link
Contributor

kcsongor commented Sep 8, 2016

I'm confused about what the added value here is. Would this be for people who essentially need the coproduct from shapeless, but prefer to write the instance by hand and/or don't want to add it to their classpath?

@kcsongor
Copy link
Contributor

kcsongor commented Sep 9, 2016

Could you elaborate on the arity argument? For the record, coproducts are binary in shapeless.

@Jacoby6000
Copy link
Contributor Author

Jacoby6000 commented Sep 9, 2016

@kcsongor here's an example use of a Copair.

It allows the caller of JsonParser to pick what kind of Copair they want back. If they primarily use eithers, they can ask for eithers, if they want a Validated because they're trying to do error accumulation they can do that.

Shapeless Coproduct is N-ary and is modeled as a singly linked list, just like HList. The difference being, HList contains an element for each type it contains, Coproducts contain only one element which may represent any of the N types. In the shapeless example, there is a Coproduct with arity 3.

When you say shapeless Coproduct is binary, are you sure you're not mixing it up with cats' Coproduct? They're very different things.

@kcsongor
Copy link
Contributor

kcsongor commented Sep 9, 2016

@Jacoby6000
"In the shapeless example, there is a coproduct with arity 3". That is not exactly true.
I can say type :+:[A, B] = Copair[A, B]
Do you agree that that type alias didn't turn your Copair into more of an n-ary sum than it was before?

type ISB = Int :+: String :+: Boolean :+: CNil <- this can be written, however, in terms of Copair, but is still the ordinary binary coproduct, just a little bit in disguise. It seems to me that what confused you was the infix type alias.

As for your JSON example, I can only repeat what I've already said, that this is what data-type generic programming is, and it is precisely what shapeless is for. For this specific example, shapeless's coproduct.
Actually, you have put the coproduct in your example, but notice how you could write the 'parse' function in terms of coproduct as well. Then what you need, is a way of going back and forth between, say, Either[A, B] and A :+: B :+: CNil. This conversion is automatically provided by shapeless - it essentially fills in your Copair type class without asking the user to do so manually.

Coproduct is modeled as a list at the type level. At the term level, however, it is just a simple value. This is like saying that Either[A, B] is modeled as a tuple. Which is true, at the type level.

@Jacoby6000
Copy link
Contributor Author

Jacoby6000 commented Sep 9, 2016

I understand and agree with you on all fronts. I don't understand what conclusion you are making though.

There is not yet a typeclass which represents the idea of Xor, Either, Validated, Coproduct...
If I'm the creator of a library, I currently have no way of generalizing those. I could return a Coproduct or an Either, or an Xor, but I can't let the user choose what they want to use.

@kcsongor
Copy link
Contributor

kcsongor commented Sep 9, 2016

My point is that if you're the author of the library, you could write your functions with shapeless, and then the user wouldn't have to instantiate any type-class, just use their Xor or Validated, as they like, the transitions would be handled generically. That being said, there is indeed no facility in cats that generalises it.
My conclusion is that data-type generic programming is not a concern of cats, so I doubt this would be justifiable as a one-off.

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.

6 participants