-
Notifications
You must be signed in to change notification settings - Fork 2
Allow Typeclasses to Declare Themselves Coherent #4
Comments
This looks like a good solution for capabilities. It would be good, though, to document the factorization approach prominently such that nobody is tempted to make Ordering coherent, otherwise we’ll soon need ReverseInt or all sorts of reverseX functions on ordered collections. |
I don't believe this would be a good thing. If a type-class has a coherence requirement, I would expect the compiler to throw an error or at least a warning instead of picking arbitrarily. We already have big problems with implicit resolution where pieces of code change behaviour depending on the order of imports. Dealing with You folks talked of An import monix.eval.Task.nondeterminism
// Now it does parallel execution
Applicative[Task].map2(task1, task2)(_ + _) But if One proposed alternative, mentioned by @adelbertc, doesn't get enough attention: not using subtype relationships, but use composition instead. I first saw this idea in Scato by @aloiscochard, which is the design that's being pushed in Scalaz 8. I have also used a variation of it in the type-class hierarchy described in monix-types, which can be seen in action in a stream-like type, a work in progress making use of higher-kinded polymorphism (see implementation). Of course, there are legitimate concerns against this approach. One of them is that you'd have to explicitly import the relationships in scope: import monix.types._
import monix.types.syntax._
def eval[F[_], A](a: => A)(implicit F: Monad[F]): F[A] = {
import F.{applicative => A}
A.pure(()).flatMap(_ => A.pure(a))
}
def filteredEvalSomething[F[_],A](a: => A, f: A => Boolean)
(implicit MF: MonadFilter[F], ME: MonadError[F,Throwable]) = {
import MF.{applicative => A, monad => M}
A.pure(())
.flatMap(_ => try A.pure(a) catch { case NonFatal(ex) => ME.raiseError(ex) })
.filter(f)
} It's annoying for sure, but then working with higher-kinded polymorphism is annoying too. In my opinion this proposal would make MTL-style code easier but at the expense of use-cases that Scala already handles well. |
If it does this, then MTL is impossible again. Motivating example: def foo[F[_]](implicit MS: MonadState[F, String], MO: MonadOption[F]) =
Monad[F] pure 42 There's a clear ambiguity here. The goal is to get rid of the compile error, not add more of them.
Whether or not parallel
There's a lot of problems with Scalaz 8's encoding. It solves many, many problems, but the encoding is quite bulky, the type errors are unclear, and as you said it comes with other use-site problems which are even more annoying. Also, fundamentally, it doesn't really solve the problem of incoherence in so much as it encodes a type level semi-lattice which may be used to resolve conflicts. |
It's not just MTL, this happens with simple stuff like: trait Functor[F[_]]
trait Bind[F[_]] extends Functor[F]
trait Apply[F[_]] extends Functor[F]
trait Foldable[F[_]]
trait Traverse[F[_]] extends Foldable[F] with Functor[F]
case class Id[A](a: A)
object Id {
implicit val IdTraverse: Traverse[Id] = new Traverse[Id] {}
implicit val IdApply: Apply[Id] = new Apply[Id] {}
}
object Example {
def example[F[_]](implicit T: Traverse[F], A: Apply[F]): Int = {
println(implicitly[Functor[F]])
0
}
example[Id]
} Adding the Coherent typeclass to Functor allows the above to compile. That's very useful. It doesn't solve all of the problems with coherency, it's still possible to pass dictionaries explicitly. It's still possible to make a higher priority implicit: trait Default[A] { def value: A }
object Default {
implicit val IntDefault: Default[Int] = new Default[Int] {
val value = 0
}
}
object Example {
implicit val MyDefaultInt: Default[Int] = new Default[Int] {
val value = 1
}
println(implicitly[Default[Int]].value) // prints 1
} So I wouldn't call this the "Coherent typeclass" but instead the "ignore ambiguities annotation" - this is useful, I would use it 👍 Using implicits for things like changing evaluation order causes problems with refactoring, which is the usual argument for coherency. The best way to handle that situation is to create a new type. |
Absent a more involved language revision to allow constructively coherent definitions (Miles has some ideas on this front), I think this is by far the best approach we could have. And I would very much use this as well.
I'm generally not in favor of newtyping just to get different implicit semantics, but I 100% agree that it's the most appropriate solution here. (edit: where by "here" I mean w.r.t. the |
It's certainly bulky, but only because it has to work with what is currently given in Scala. If you imagine a hypothetical language extension, similar to what we have here with Though perhaps this is what |
I'm guessing the plan is that in the coherent case, where it will not error on ambiguous Implicits, instead there will be some well-defined (if objectively arbitrary) and specified set of rules used to select the implicit? If yes, that might mitigate the "what if I want to override an implicit that a library shortsightedly marked coherent" concern... |
One concern I have about this approach is it seems to be an ad-hoc mechanism nailed on top of the existing implicit resolver. Maybe such a concern is ill-founded, this is just my intuition speaking at this point. |
Though it seems an interesting and promising concept, relying on a programmer's discipline to check the conditions for coherence is not scalable; managing programmer team discipline over organisational change, scope an time is extremely difficult. Without embedding this check into the compiler, I strongly believe this will increase the problems with implicits even more! I inherited a Scala code base of about 5 years old with several "historical" layers of maturity. Nor the IntelliJ Scala plugin or the Scala Eclipse version are able to offer reliable support; they can't handle the depth of the implied type proofs and either halt or crash or misidentify the culprit. Only compiling all code can "prove" what happens. I do see a use for implicits, but it is too easy to abuse them. Therefore any proposal extending their use not accompanied with compiler support enforcing the rule of application will invariably incur heaps of technical debt. |
i recently run into problems using cats where i imported the same syntax extension (via an implicit class) over two different inheritance paths and had a hard time figuring out why the compiler told me the extension method does not exist. i think those two problems are related: the compiler does not know two things (a parent typeclass or an extension) are actually the same, codewise. it might make more sense to attack the problem at the point where the confusion occurs instead of where it originates from: in the functor/traversable/monad example it might make more sense to attach the information that they are inheriting "the same" functor to travesable and monad, instead telling the compiler that whereever the functor occurs it's probably the same one. |
perhaps this has been discussed before, and discarded, http://docs.scala-lang.org/sips/pending/trait-parameters.html, but what about using implicit arguments on the trait to achieve composition of typeclasses?
so, instead of
we could do
but this means that at the call site we would need to do this:
but also means that there will be no ambiguity as what Functor implementation is being used the only downside I see, besides that I don't know what would take to make this happen, is that we would need to provide implicits for all the typeclasses required, but in a way I think it adds to clarity |
I agree it is a worry. Technically the check cannot be put into the compiler, since a compiler has no knowledge what program is. Maybe we can put it into a build tool. But that's a long way off, for sure.
It would be interesting to know more about this. Implicit parameters are the core of Scala. We cannot simply suppress them and the recent addition of implicit function types has made them even more powerful than before. Implicit conversions we could and probably should restrict further. So, to make progress in the field it would be really important to learn specifics of past mistakes. This would help us develop patterns or maybe even compiler-checked rules that avoid similar mistakes in the future. |
Looks like shapeless's |
I think you are right that implicits are at the core of the Scala language, but implicit resolution is definitely an area in which the compiler could provide more help - this does not necessarily have to be in the form of static checks. Consider that the tool that scalac offers currently is the incredibly verbose output of Perhaps compiler plugins like tek/splain and Scala Clippy could be used as inspiration for more concise and user-friendly output in Dotty, or even a slightly more graphical output in keeping with the beautiful new compilation error messages? However, since this is veering quite a long way off topic I think it would probably be better to discuss this further on https://contributors.scala-lang.org if there is some interest in doing so? |
Yes, let's take discussion about implicit best practices to https://contributors.scala-lang.org. |
What I took away from the very good discussion here (and also on the Scala reddit) is that an unchecked I now think that we should try to solve the question first how to check that usage of a typeclass is actually coherent. To be decidable by the regular compiler, the check has to be quite restrictive. For instance, we'd almost certainly need a rule that implicit definitions that make a type T an instance of the coherent type class C have to be defined in the same compilation unit as either T or C. But I think a restrictive verification of coherence is better than opening the floodgates and let every type class be coherent by declaration. |
I like this. It's very reminiscent of GHC orphan instance warning (See https://wiki.haskell.org/Orphan_instance, https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/separate_compilation.html#orphan-modules-and-instance-declarations) |
That's my primary concern expressed in my comment above.
I also like this rule very much. I can think of problems with such restrictions though, for example the reason for why I have a So what happens is that if you have defined a Of course, using shims is far from ideal, it sucks actually and this isn't a problem that Haskell has AFAIK. Just mentioning this for awareness. |
@odersky A restriction to only allowing coherence when the instances are defined within the same compilation unit is going to make something like this almost impractically narrowly applicable. The whole point of typeclasses is that they're an open extension mechanism, but that benefit is completely lost if you're trying to achieve same-unit coherence at the same time. In practice, I'm relatively confident that even frameworks which highly value coherence (like Scalaz or Cats) would simply forego that potential benefit, since the costs would be too high. I agree that a system which implements actual checks for coherence, rather than functioning as an assertion, would be strongly preferable. But it's important that such a system doesn't come at the expense of all of the other benefits of typeclasses. Edit: I misread the proposal. See below. |
@djspiewak I think "impractically narrowly applicable" is a bit of an exaggeration, as far as I know, it's considered good practice in the Haskell community to avoid orphan instances. |
@smarter Rereading @odersky's proposal, I see that it suggests it might be in the compilation units for either |
Why couldn't you have the |
You could, but that would force users to bring that instance in, whether they want it or not. That might be a tradeoff that frameworks would make, but Scalaz decided explicitly against doing that back when Scalaz 7 was designed (Scalaz 6 used to bring in stdlib instances by default), and Cats has held to that design for largely the same reasons. |
If it's in the same compilation unit but in a separate object, it wouldn't force users to bring the instance in, right? |
@smarter Actually even better, I suppose it could be defined as a trait which is later mixed into objects elsewhere (e.g. Actually for that very reason, it sort of surprises me that same-compilation unit would be a sufficient restriction. |
Good point, maybe besides same-compilation unit we also need to require the typeclass instance to be defined in a sealed/final class or an object? |
That would be annoying, but wouldn't compromise any of the things Scalaz/Cats want to do with their import organization. In fact, we might actually get to see more use of the |
I think a more convincing reason we don't define instances in the companion
object of the type class specifically (defining it in the object for the
data type is fine) is if we put the Monad instance for Option in object
Monad it would not resolve if we only asked for say, Functor.
This generalizes to defining instances in the companion object of a type
class and asking for the instance of a super class.
…On Mar 7, 2017 10:32, "Guillaume Martres" ***@***.***> wrote:
Good point, maybe besides same-compilation unit we also need to require
the typeclass instance to be defined in a sealed/final class or an object?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<https://github.com/lampepfl/dotty/issues/2047#issuecomment-284813571>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABRW9FGRdOScDG4JGSMNyl1K-rFlwQSdks5rjaLOgaJpZM4MSG3t>
.
|
I agree. There is an alternative version of this proposal that moves the coherency decision from definition side of the class to the use-site. This has an advantage of not polluting the entire code-base if programmers aren't disciplined enough, as it will only harm a single method. This can be achived by defining a magical def makeRBTree[T](implicit a: Coherent[Ordering[T]]) I think this alternative provides a safer mechanism to be used in big code-bases. It will be slightly harder to implement due to more magic involved inside compiler |
@DarkDimius I had an idea very similar to this a few months ago. It's actually implementable on current scalac, but it does of course require some significant use of dark magic to manipulate the compiler's internal processes via reflection in a macro, but it is possible. I don't really like it as much as the declaration-site proposals though. And particularly if it's possible to have a checked coherence with just same-file restrictions. |
@djspiewak yup, that's what I mean by "assert". I, the programmer, tell the compiler that they do. If they don't, my problem. |
@djspiewak Program equivalence is certainly not decidable in general for Turing-complete languages, though many instances are more restrictive. But yeah asserting coherence makes sense—one extra example from @adelbertc is below. Also, let's keep in mind both scalaz and cats have maintained coherent hierarchies, so there's a strong case (two groups of candidate users) for giving careful programmer escape hatches and trust they manage.
I suspect so, but instead of showing why you want two instances for type BTW, code in this spirit requires OverlappingInstances in Haskell (though maybe not this snippet).
|
@Blaisorblade I have updated the pseudo-code to include a more refined inequality test of instances. |
@odersky Thanks — I agree the test should be I still have the same quibbles on how But we don't need to reinvent Here, for soundness, we want
In particular, the test in the pattern matcher must be conservative in the same direction, so that Dotty/Scalac only reject matches that can never be triggered. Allowing further matches that could be rejected is okay. Rejecting matches that can be triggered is not OK. Why this implementation is not good enough:
The freshening helps a bit (though I wasn't worrying about that yet). Look again at
I want |
This would be draconian, as it would mean that even implementing a coherent trait with two different trait arguments would violate disjointness. After all, you can always define a class that inherits both traits. But I don't think this would be required anyway. What could the common type of T1 and T2 do that would violate coherence? |
First: maybe we should split a separate issue for coherence checking—the original proposal did not include it and the topic is non-trivial.
That restriction seems required to enforce coherence, in particular the invariant:
In my example (assuming class C1[A, B, C] extends Arbitrary[A => (B => C)] { def f = 1; ... }
class C2[A, B, C] extends Arbitrary[(A => B) => C] { def f = 2;... } you can have two instances
I'm confused, but if you mean But more in general, the needed restriction might well be draconian—but we should not ask for the impossible and look at GHC to understand well the "simple" case. Scare quotes are appropriate—understanding that simple case well enough might take weeks. In GHC, checking coherence when instances are declared indeed rejects my example and many others, including things which are pretty desirable. I suspect the example with I'm no expert there, but a quick look suggests we should check coherence at instance search time. Let's look at Haskell'98 and at
see https://downloads.haskell.org/~ghc/8.0.2/docs/html/users_guide/glasgow_exts.html#instance-resolution I hope we agree that's extremely restrictive.
See https://downloads.haskell.org/~ghc/8.0.2/docs/html/users_guide/glasgow_exts.html#instance-overlap. |
Sure. And it would be covered by the new disjoint rules.
That's what I intended to convey when I said "instantiatable type variables", but maybe that was not very clear. |
Ah OK! So we're in vociferous agreement. And then I must read
as "there is substitution sigma instantiating of type variables in T and U such that sigma T <: sigma U"? I must catch up with Dotty lexicon. |
This is probably very stupid point. But who knows. Spring has very simple feature which is not provided by Scala implicit system. If I have an interface A and I will configure SpringContext to contain multiple implementations which could be provided it fails on startup. But if I @Autowired constuctor value as of type List[A] than SpringContext will inject List of all possible implementations instead of failing. Typically it is used for plugin systems. If such feature would be there than developer could declare this way he wants to accepts multiple implementations and decide himself inside method implementation what to do. It would be similar like letting C++ developers to solve inheritance diamond themselves. There could be special collection like type for it. It just came to my mind. EDIT: I think it wouldn't help at all. You are after something else. |
re: design choices and boilerplate If given two choices:
I would choose the first one every time. Code is re-read, re-organized, and re-written more often than it is written the first time, in any non-trivial project. The brittle-ness of various common patterns in Scala is a real problem. re: ugliness of having to type something like And now for a related digression: Because Dotty now has union types, and we are discussing Top and Bottom types... Removing This also leads to exciting possibilities like a box-less option (and Either)
An inhabited bottom type may be required in some cases with variant types, and honestly I haven't thought through that. Perhaps in the case where an inhabitable bottom is required, one could just use So... parting thoughts on a new Top type for the Coherence problem: Since Dotty has Union types, we no longer have to think of all of them as in a pure inheritance hierarchy. For example, Instead of something like:
You can have:
(the difference is subtle here, but in more complicated cases union types give more flexibility -- e.g. if one adds in "nullability" and "identity" orthogonal concepts, these don't fit into an inheritance tree nicely) |
Regarding parametric top, I think it would be beneficial to have a version of |
I'm afraid I fail to see how. If you have a generic method For reference, I'm looking again at
from https://github.com/lampepfl/dotty/issues/2047#issuecomment-286187515, but I don't think this makes a difference. |
Yes. The key was "if the underlying type is already a ref type." If I have class Foo(val s: String) extends AnyVal then passing |
Even if you have no method, you still have to support |
Ah, that ruins it 😞 |
@sjrd I think that @TomasMikula's suggestion in fact works, which is cool!
Ah, I saw that and wondered what that means. Thanks!
Not entirely... Parametricity forbids class Foo(val s: String) extends ParametricAnyVal
class Foo2(val s: String) extends ParametricAnyVal
new Foo("hi").isInstanceOf[Foo] // I think we *could* allow this without violating parametricity.
//But this example is always `true`.
new Foo("hi").isInstanceOf[Foo2] //allowable, always false.
//But I'm sure `isInstanceOf` *cannot* be a member of `ParametricAnyVal`, because...
def weirdId[T <: ParametricAnyVal](t : T) = {
//x.isInstanceOf[Foo] // this must 100% be a compile error to support parametricity
x
}
def weirdId2[T <: ParametricAny](t : T) = {
//x.isInstanceOf[Foo] // this must 100% be a compile error to support parametricity
x
} Here, I mean, parametricity specifically means that It might still be hard to implement the optimization, especially if It's hard to construct examples where class Foo3(s: String) extends Foo(s) //with ParametricAnyVal //please, still a parametric value class.
def f(x: Foo) = x.isInstanceOf[Foo3] //actually requires a runtime test. Are such hierarchies allowed for value classes? Do we need them for |
@Blaisorblade It would be cool if this could be salvaged!
All value classes are |
isInstanceOf is indeed no problem. But I have not seen any suggestions for
a top class hierarchy yet and I dread that we will over-complicate things.
What are the suggestions?
…On Thu, Apr 27, 2017 at 10:38 PM, Tomas Mikula ***@***.***> wrote:
@Blaisorblade <https://github.com/Blaisorblade> It would be cool if this
could be salvaged!
Are such hierarchies allowed for value classes?
All value classes are final, at least up to Scala 2.12.2.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<https://github.com/lampepfl/dotty/issues/2047#issuecomment-297832404>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAwlVm9wX-Y52n1R0QhVH8wOW1_8YKnUks5r0Py1gaJpZM4MSG3t>
.
{"api_version":"1.0","publisher":{"api_key":"
05dde50f1d1a384dd78767c55493e4bb","name":"GitHub"},"entity":
{"external_key":"github/lampepfl/dotty","title":"
lampepfl/dotty","subtitle":"GitHub repository","main_image_url":"
https://cloud.githubusercontent.com/assets/143418/17495839/a5054eac-5d88-
11e6-95fc-7290892c7bb5.png","avatar_image_url":"https://
cloud.githubusercontent.com/assets/143418/15842166/
7c72db34-2c0b-11e6-9aed-b52498112777.png","action":{"name":"Open in
GitHub","url":"https://github.com/lampepfl/dotty"}},"
***@***.*** in #2047:
@Blaisorblade It would be cool if this could be salvaged!\r\n\r\n\u003e Are
such hierarchies allowed for value classes?\r\n\r\nAll value classes are
`final`, at least up to Scala 2.12.2."}],"action":{"name":"View
Issue","url":"https://github.com/lampepfl/dotty/issues/
2047#issuecomment-297832404"}}}
--
Prof. Martin Odersky
LAMP/IC, EPFL
|
@odersky I was working based on one of your proposals
from https://github.com/lampepfl/dotty/issues/2047#issuecomment-286187515, extended with some unspecified For reference, the constraint from @TomasMikula is
|
On Thu, Apr 27, 2017 at 11:02 PM, Paolo G. Giarrusso < ***@***.***> wrote:
@odersky <https://github.com/odersky> I was working based on one of your
proposals
Any
|
AnyObj
/ \
AnyVal AnyRef
from #2047 (comment)
<https://github.com/lampepfl/dotty/issues/2047#issuecomment-286187515>,
extended with some unspecified ParametricAnyVal. But ParametricAnyVal
adds no methods to Any, so it could be the same. Maybe you can specify
"valueness" with an annotation.
I believe annotations in general are a cop-out, syntactic sugar to
dissimulate semantic content.
… For reference, the constraint from @TomasMikula
<https://github.com/TomasMikula> is
I think it would be beneficial to have a version of AnyVal without methods
as well, i.e. a value class that is not a subtype of AnyObj.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<https://github.com/lampepfl/dotty/issues/2047#issuecomment-297838646>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAwlVg7UmEGkY6dkh7i0w9aT0cQJxJdoks5r0QJ8gaJpZM4MSG3t>
.
{"api_version":"1.0","publisher":{"api_key":"
05dde50f1d1a384dd78767c55493e4bb","name":"GitHub"},"entity":
{"external_key":"github/lampepfl/dotty","title":"
lampepfl/dotty","subtitle":"GitHub repository","main_image_url":"
https://cloud.githubusercontent.com/assets/143418/17495839/a5054eac-5d88-
11e6-95fc-7290892c7bb5.png","avatar_image_url":"https://
cloud.githubusercontent.com/assets/143418/15842166/
7c72db34-2c0b-11e6-9aed-b52498112777.png","action":{"name":"Open in
GitHub","url":"https://github.com/lampepfl/dotty"}},"
***@***.*** in #2047:
@odersky I was working based on one of your proposals\r\n```\r\n Any\r\n
|\r\n AnyObj\r\n / \\\r\nAnyVal AnyRef\r\n```\r\nfrom
https://github.com/lampepfl/dotty/issues/2047#issuecomment-286187515,
extended with some unspecified `ParametricAnyVal`. But `ParametricAnyVal`
adds no methods to `Any`, so it could be the same. Maybe you can specify
\"valueness\" with an annotation.\r\n\r\nFor reference, the constraint from
@TomasMikula is\r\n\r\n\u003e I think it would be beneficial to have a
version of AnyVal without methods as well, i.e. a value class that is not a
subtype of AnyObj."}],"action":{"name":"View Issue","url":"https://github.
com/lampepfl/dotty/issues/2047#issuecomment-297838646"}}}
--
Prof. Martin Odersky
LAMP/IC, EPFL
|
My idea was this
|
It's strange to me that everyone was so busy with making rules for a global coherence requirement, If we forget about trying to impose global coherence, and come back to the ambiguity question, let's ask – what prevents us from choosing an arbitrary instance in case of ambiguity? If we knew, we could pick either of them. But we could just directly check if they're the same! Let's start with the usual suspects: trait Functor[F[_]] { def map[A, B](f: A => B): F[B] }
trait Monad[F[_]] extends Functor[F]
trait Traversable[F[_]] extends Functor[F]
trait Maybe[A]
object Maybe {
trait FunctorImpl extends Functor[Maybe] {
final def map[A, B](f: A => B): Maybe[B] = ???
}
implicit val maybeMonad: Monad[Maybe] = new Monad[Maybe] with FunctorImpl { ??? }
implicit val maybeTraversable: Traversable[Maybe] = new Traversable[Maybe] with FunctorImpl { ??? }
} When we try to summon an implicit value, we'll get an ambiguity error. implicitly[Functor[Maybe]] // error We know that both implicit val maybeMonad: Monad[Maybe] with FunctorImpl = new Monad[Maybe] with FunctorImpl { ??? }
implicit val maybeTraversable: Traversable[Maybe] with FunctorImpl = new Traversable[Maybe] with FunctorImpl { ??? } Or just omit the signatures and let the compiler infer the narowest type: implicit final val maybeMonad = ???
implicit final val maybeTraversable = ??? Now the compiler, too, has direct unfalsifiable evidence that both of the implementations are the same, it only needs to glance at the type signature to verify it. At this point it can safely pick either instance, knowing that they both point to the same trait. implicitly[Functor[Maybe]] // good, all the instances are just FunctorImpl in disguise In more detail, when encountering an ambiguity over some type For library authors, the scheme would entail two minor changes to their workflow:
For users of implicit there would be no changes at all – they would just suddenly stop getting ambiguity errors! They wouldn't have to specify coherence domains in their implicit summons and they wouldn't lose the ability to declare orphans and local instances. This scheme does not substitute for coherence in all cases, but it solves the problem of ambiguity in a straightforward and obvious manner without imposing any draconian restrictions on the users. Implementation: this scheme can be implemented easily today as an implicit macro or as a compiler plugin. Caveats: If an implicit value is taken and re-assigned to a variable with a widened type, it's disambiguation properties are lost. However, coherent domains exhibit the same properties if an implicit in a domain is re-assigned to a domainless value. A variant of this scheme can also be implemented if Scala gets first-class delegation, e.g: final val functorMaybe: Functor[Maybe] = ???
implicit val maybeMonad: Monad[Maybe] with (Functor[Maybe] by functorMaybe.type)
implicit val maybeTraversable: Traversable[Maybe] with (Functor[Maybe] by functorMaybe.type) Would be resolved because the singleton types which implement the Functor class are proven the same. Addendum: to prevent circumventing finality with e.g. |
@Kaishh FWIW, what you just described is basically (delta few integration details with Dotty) the encoding of the TC system in Scalaz8. |
@aloiscochard This scheme applies only to instances, i.e. [not similar, scalaz defines type class relations here. The thing scalaz 8 does vs. ambiguity is erase subtype relations with a newtype, then redefine custom type class relations via implicit conversions, which is different from an error recovery strategy that tries to resolve any subtype relation only in case of ambiguity] |
Isn't that he opposite of coherence? Not to mention terrifying (non-deterministic behavior).
This is how Haskell does it, and seems to work well enough. |
Motivation
Unlike Haskell, Scala allows multiple implicit instances of a type. This makes implicits cover a lot more use cases. Besides type classes, implicits can also express
When used for implementing typeclasses, implicits support different implementations of a type class in different modules. For instance,
Boolean
could implementMonoid
withtrue
and&
in one part of a program and withfalse
and|
in another.By contrast, Haskell's type classes have a coherence requirement which states that a type can implement a type class only in one way anywhere in a program. Coherence restricts what one can do with type classes. It feels a bit anti-modular since it establishes a condition for "a whole program" (in Scala or Java one would not even know what that is!). But it also solves two problems of Scala's unrestricted use of implicits.
The first problem is that one can accidentally mix two different implicit implementations where this is non-sensical. The classic example is a priority queue which requires an
Ordered
element type. If one declares priority queue like this:there's no way to guarantee that the two queues in a
union
operation agree on the ordering of typeT
. This can lead to nonsensical results. I believe this is a bit of a red herring however, as it is perfectly possible to design data structures that do not suffer from that problem by using some nesting (or functorizing, to borrow some ML terminology). For priority queues, Chris Okasaki's scads is a nice solution, which generalizes readily. In essence, all that's needed is to move the type parameterT
one level further out:So far, so good. But there's also a second problem which is in a sense the dual of the first. The first problem was "how do we prevent some programs from compiling when implementations are not coherent"? (i.e. we have multiple implementations of the same type class). The second problem is "how do we get some programs to compile when implementations are coherent"? In Scala, implicit resolution may lead to ambiguities, which are compile-time errors. But in a coherent world, ambiguities do not matter: picking any one of multiple applicable alternatives would give the same observable result. So if we know that all implementations of some type are coherent, we could suppress all ambiguity errors related to implicit searches of that type.
This is a real issue in practice, which mars attempts to implement in Scala typeclass hierarchies that are standard in Haskell. #2029 is a good starting point to explore the issue. Here's a somewhat more figurative explanation of the problem: Say you want to model capabilities representing driving permits as implicit values. There's a basic capability "has drivers license", call it DL. There are two more powerful capabilities "has truck driver's license" (TL) and "has cab license" (CL). These would be modelled as subtypes of the "drivers license" type. Now, take a function that requires the capabilities to drive a truck and to drive a cab, i.e. it takes two implicit parameters of type TL and CL. The body of the function needs at some point the capability of driving a car (DL). In the real world, this would be no problem since either of the function's capabilities TL and CL imply the capability DL. But in current Scala, you'd get an ambiguity, precisely because both implicit parameters of types TL and CL match the required type DL. In actual Scala code, this is expressed as follows:
Compiling this results in an ambiguity error:
#2029 and @adelbertc's note contain discussions of possible workarounds. One workaround uses aggregation instead of subtyping. The other tries to bundle competing implicit parameters in a single implicit in order to avoid ambiguities. Neither approach feels both simple and scalable in a modular fashion.
Proposal
The proposal is to introduce a new marker trait
scala.typeclass.Coherent
.If a type
T
extends directly or indirectly theCoherent
trait, implicit searches for instances of typeT
will never issue ambiguity errors. Instead, if there are several implicit values of typeT
(where no value is more specific than any other) an arbitrary value among them will be chosen.For instance, the
Driving
example above can be made to compile by changing the definition ofDL
toSoundness
The rule for dealing with coherence makes some implicit selections unspecified. A global coherence condition is needed to ensure that such underspecifications cannot lead to undetermined behavior of a program as its whole. Essentially, we need to ensure that if there is more than one implicit value implementing a coherent type
T
, all such values are indistinguishable from each other. (In)distinguishability depends on the operations performed by the program on the implicit values. For instance, if the program compares implicit values using reference equality (eq
), it can always distinguish them. One good rule to follow then is to avoid callingeq
, as well as all other operations defined inAnyRef
on values of coherent types. Furthermore, one needs to also ensure that all members of a coherent type are implemented in the same way in all implicit instances of that type.It's conceivable that sufficient coherence conditions can be established and checked by a tool on a project-wide basis. This is left for future exploration, however. The present proposal defers to the programmer to check that coherence conditions are met.
Mixing coherent and normal abstractions
Sometimes one has conflicting requirements whether a type should be coherent or not. Coherency avoids ambiguity errors but it also prohibits possibly useful alternative implementations of a type. Fortunately, there's a way to have your cake and eat it too: One can use two types - a non-coherent supertype together with a coherent subtype.
For instance one could have a standard
Monoid
trait that admits multiple implementations:One can then declare a coherent subtrait:
One could also several coherency domains by using a class instead of an object for
Canonical
and instantiating that class for each domain.Alternatives
@djspiewak has proposed a different approach to coherence based on parameterizing implicit definitions with coherency domains. #2029 contains a discussion of this approach in relation to the present proposal.
Implementation Status
The proposal is implemented in #2046.
The text was updated successfully, but these errors were encountered: