-
-
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
Workaround for ambiguous implicits when using MTL type classes, fixes… #1379
Conversation
…tive, and MonadCombine
OK so the build is failing, it's unfortunate we run
Which when looking at the EDIT Nevermind found it. Was referencing a |
Current coverage is 91.61% (diff: 95.21%)@@ master #1379 diff @@
==========================================
Files 239 239
Lines 3596 3673 +77
Methods 3528 3609 +81
Messages 0 0
Branches 67 63 -4
==========================================
+ Hits 3299 3365 +66
- Misses 297 308 +11
Partials 0 0
|
@adelbertc it looks like there's a merge conflict. I think I'm 👍 on this change. As documented in #1210, it's currently pretty much impossible to have elegant MTL-style code with Cats. I don't particularly like that it's different than how we are encoding type class hierarchies elsewhere, but I don't know of a better approach, and I guess if nothing else it's a first attempt at an alternative type class hierarchy encoding strategy that maybe we'll end up wanting to use elsewhere. One thing that we could do is change |
@ceedubs Yeah it's unfortunate it's a different encoding, but it leads to niceties. If you look at Re: changing the member to implicit, I originally had it that way but for the same reason as you stated I ended up making it non-implicit. That way you can opt in if you want to bring the instance into scope with On a related note, I am OK if folks end up not liking having the alternate encoding, I would probably just end up making a separate Merge conflict should be easy enough to resolve, but would like to get buy-in from other folks first. Since this touches a bunch of stuff and makes some fairly big changes, I'd like to get three more 👍 s on top of @ceedubs before proceeding. Pinging some folks who've spoken up on #1210 to get thoughts/review @johnynek @alexandru |
Personally I don't think that there's much point in having MTL classes in Cats if they are mostly unusable, so I support the approach presented in this PR. If others really don't want to bring this style of type class encoding into cats, then at that point I'm not sure the MTL classes should even exist in Cats. |
@ceedubs I'm beginning to prefer having a separate If we do go forward with this, we also need to codify what constitutes an "MTL class" that we want to remove. The obvious ones are ApplicativeError, MonadError, MonadReader, MonadState, MonadWriter. The difficulty lies in the other ones I modified in this PR: MonadPlus, Alternative, FunctorFilter, TraverseFilter, and MonadFilter. With the obvious ones ripped out, you can still get ambiguous implicits from a reasonable combination of these:
Even more ambiguities arise with the usual MTL type classes. to name two. That being said there's also been some issues surrounding the mere existence of some of these type classes:
And also some additions of similar ones:
I think we should take this PR to discuss how we want to approach these moving forward. (on a side note I think these problems succinctly demonstrate a big flaw in the subtype approach to encoding type classes). I think Spire has a lot of this kind of branching of type classes, would be curious to hear @non and @tixxit 's thoughts on the matter. |
@adelbertc simply |
@ceedubs Yep |
In my view, a reasonable rule would be to allow at most one subclass typeclass within cats, and document or have some tool check that. I don't like having some vague notion of mtl since I don't see how ApplicativeError or Traverse can be considered mtl yet trigger the issue. A more invasive solution is what we have discussed (methods to return subtype typeclasses rather than extension). The algebra issue is not as accute because generally you don't accept many different algebras. You just take the least specific you need for the method so I don't think it comes up like it does here. |
What does that mean for stuff like |
The benefit of the invasive solution is that it is simpler to review and implement generally. The downside is that it seems like a much bigger change for library consumers. Finally, I'm not sure this is totally a disaster currently. Yes you et ambiguous implicits for Monad if you accept implicit MonadCombjne and MonadError but you can manually resolve such conflicts by explicitly choosing one right? And explicitly choosing one is likely what happens with the |
@johnynek It makes the ergonomics of using Monads very poor. It destroys for-comprehensions, and while you can still explicitly write out Note that it is not just the typical MTL type classes that completely break for-comprehension, just getting ambiguous |
Wrote a more detailed overview of issues with subtyping + type classes: https://github.com/adelbertc/faq/blob/2e8c7953422401879c5cfcf3ffc2d24675ec0fd8/src/main/tut/subtype-typeclasses.compiled.md EDIT Now on Typelevel blog with some more info: http://typelevel.org/blog/2016/09/30/subtype-typeclasses.html |
When I first read that ticket, I didn't even know what MTL stands for, all I could infer is that the inheritance-based encoding creates problems, because indeed I bumped into some problems myself. But that's a problem, as how can you explain the separation to people unfamiliar with this? Unless the explanation is that Personally I like this composition based encoding, even if bringing implicits in the local scope is required, however right now I have mixed feelings about having to work with 2 type-class encodings. The uninitiated can then wonder why is I don't know what the right answer is, but right now I'm thinking that consistency might be better than optimizing some use-cases at the expense of confusion, even if we end up working with |
@johnynek Re: your comment on #1210 (comment), as was pointed out to me by @jbgi that the hierarchy need not be baked into the type classes themselves as that would restrict extensibility, instead it could be moved out albeit at the cost of requiring an import on every use site now. See: https://github.com/scalaz/scalaz/blob/series/8.0.x/base/src/main/scala/BaseHierarchy.scala and https://github.com/scalaz/scalaz/blob/series/8.0.x/base/src/main/scala/package.scala - using the hierarchy requires an |
Ping. How do we want to proceed with this? cc @non EDIT I am for the invasive change as it solves the root of the problem as opposed to putting lipstick on the proverbial pig (maybe a bit harsh but yeah :P ) |
I'm pretty nervous about this. I feel like the current situation is usable I think this problem deserves a clear doc with what seem to me like at
|
I think this is too drastic a change to merge without exploring the space of possibilities more thoroughly. At least with respect to the motivating example here ( At least part of the problem stems from the fact that we ask for the instances of This gets along very nicely with the way that standard instances are defined in Cats: rather than providing instances for This solves the local consistency problem, but we still have to deal with ambiguities which arise from the way that syntax is applied: we have syntax for But we can fix this by creating more specific syntax: syntax for // Nb. all syntax is in scope ...
import ListInstances._, FunctorSyntax._, MonadSyntax._, TraverseSyntax._, MTSyntax._
def foo[F[_]](implicit MT: Monad[F] with Traverse[F]): F[Int] = {
for {
a <- MT.pure(10)
b <- MT.pure(20)
} yield a+b
} Full example here. |
I remember talking to @S11001001 about this on IRC.. I think there was concern that Another issue is that doesn't work for data types where the instances are definitely separately, such as monad transformers. scala> import cats._
import cats._
scala> import cats.implicits._
import cats.implicits._
scala> def foo[F[_]](implicit F: Monad[F] with Traverse[F]): Int = 42
foo: [F[_]](implicit F: cats.Monad[F] with cats.Traverse[F])Int
scala> foo[Option]
res0: Int = 42
scala> import cats.data.OptionT
import cats.data.OptionT
scala> foo[OptionT[List, ?]]
<console>:21: error: could not find implicit value for parameter F: cats.Monad[[β]cats.data.OptionT[[+A]List[A],β]] with cats.Traverse[[β]cats.data.OptionT[[+A]List[A],β]]
foo[OptionT[List, ?]]
^
scala> type Alias[A] = OptionT[List, A]
defined type alias Alias
scala> foo[Alias]
<console>:22: error: could not find implicit value for parameter F: cats.Monad[Alias] with cats.Traverse[Alias]
foo[Alias]
^
scala> Monad[OptionT[List, ?]]
res2: cats.Monad[[β]cats.data.OptionT[[+A]List[A],β]] = cats.data.OptionTInstances$$anon$2@465c0f8d
scala> Traverse[OptionT[List, ?]]
res3: cats.Traverse[[β]cats.data.OptionT[[+A]List[A],β]] = cats.data.OptionTInstances2$$anon$3@2bd44feb or simplified: scala> import cats._
import cats._
scala> import cats.implicits._
import cats.implicits._
scala> def foo[F[_]](implicit F: Monad[F] with Traverse[F]): Int = 42
foo: [F[_]](implicit F: cats.Monad[F] with cats.Traverse[F])Int
scala> case class Foo[A](a: A)
defined class Foo
scala> implicit def monadFoo: Monad[Foo] = ???
monadFoo: cats.Monad[Foo]
scala> implicit def traverseFoo: Traverse[Foo] = ???
traverseFoo: cats.Traverse[Foo]
scala> foo[Foo]
<console>:23: error: could not find implicit value for parameter F: cats.Monad[Foo] with cats.Traverse[Foo]
foo[Foo]
^ |
I think the fact that separately defined instances don't conform to If you can provide an alternative proof of consistency that'd be fine. One way to do that would be to construct a local |
That being said, this appears to give second class status to monad transformers or any type where instances are defined separately ( I asked in the Idris IRC channel to see what happens in this bit of code (which is probably syntactically incorrect but I hope communicates the message): functorOnly :: Functor f => f ()
monadAndTraverse :: (Monad f, Traverse f) => f ()
monadAndTraverse = functorOnly
x = monadAndTraverse :: List Unit
y = monadAndTraverse @{myOwnMonad} @{myOwnTraverse} specifically how If "Cats" ends up deciding global uniqueness (interesting read on that btw: http://blog.ezyang.com/2014/07/type-classes-confluence-coherence-global-uniqueness/) is not something we care about (at the time of this writing I know I do, as well as a couple others) I think we need to come up with a more satisfying solution than making clients construct instances on the fly. Perhaps we can play games with syntax prioritization or something. |
While the arguments of @milessabin make sense, I'd like to draw attention to the fact that using Alternatives like Scalaz will be around for some time. Along with libraries that expose shims to be converted back and forth to Cats and Scalaz. Having functionality that depends on combinations like |
@adelbertc saw it coming, now that MTL classes are in cats-mtl the encoding he mentioned is in use. |
@edmundnoble i owe you a beer. or something beer-esque. awesome job man! |
… #1210
Putting this up so folks can see what I'm doing as I do it. Here's the checklist of type classes I found that I'll be doing the change for:
More details on the change:
Before the MTL type classes were encoded as usual:
This change changes the encoding for the above type classes into:
I asked around and this seems to be a not uncommon approach [1] [2] .
Caveats:
Before given
MonadReader[F, R]
you would get aMonad[F]
instance automagically, meaning this works:With this
MonadReader
is completely out of the subtype hierarchy so you now either need to do one of the following:I think this is well worth it to free up the ergonomics of using the MTL type classes (see #1210). The downside is these type classes get treated specially, but weighing the benefits against the cost I like this a lot more. I've thought about this for a long while and I've yet to come up with a better encoding than this given the current subtype hierarchy.