Skip to content

Conversation

@ceedubs
Copy link
Contributor

@ceedubs ceedubs commented Sep 2, 2018

One of the benefits of type class-based equality is that the compiler
can enforce that you only check equality on types for which it is
meaningful (as opposed to universal equality such as Java's equals
that will allow you to compare any two objects even if it's a
meaningless comparison due to a typo).

Having an implicit universal Equiv instance throws away this benefit.
With this instance in scope (and it always is, since it's in the
companion object), the compiler will happily let you compare two Any
instances, two Function1 instances, etc, even though doing so is
probably a mistake. Also, even if you have defined a well-behaved
Equiv instance for your type, if you forget to import it, the
universal instance will jump in to take its place without the compiler
warning you.

Here is a recent tweet from @jvican on the subject.

This also came up in a discussion in the Cats
repo that I started recently about what it would take for Cats to start
using Equiv/PartialOrdering/Ordering from the standard library
instead of having its own version of these type classes. I'm a bit
hesitant to link to that conversation here, since it goes well beyond
the scope of this PR, but it is relevant.

If desired, I'll happily change this to a PR to remove this instance
altogether for Scala 2.13, but I wasn't sure whether it was too late to
introduce that change.

One of the benefits of type class-based equality is that the compiler
can enforce that you only check equality on types for which it is
meaningful (as opposed to universal equality such as Java's `equals`
that will allow you to compare any two objects even if it's a
meaningless comparison due to a typo).

Having an implicit universal `Equiv` instance throws away this benefit.
With this instance in scope (and it always is, since it's in the
companion object), the compiler will happily let you compare two `Any`
instances, two `Function1` instances, etc, even though doing so is
probably a mistake. Also, even if you have defined a well-behaved
`Equiv` instance for your type, if you forget to import it, the
universal instance will jump in to take its place without the compiler
warning you.

[Here](https://twitter.com/jvican/status/1034529059911426048?s=19) is a
recent tweet from @jvican on the subject.

This also came up in a
[discussion](typelevel/cats#2455) in the Cats
repo that I started recently about what it would take for Cats to start
using `Equiv`/`PartialOrdering`/`PartialOrder` from the standard library
instead of having its own version of these type classes. I'm a bit
hesitant to link to that conversation here, since it goes well beyond
the scope of this PR, but it is relevant.

If desired, I'll happily change this to a PR to remove this instance
altogether for Scala 2.13, but I wasn't sure whether it was too late to
introduce that change.
Copy link
Contributor

@NthPortal NthPortal left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Member

@jvican jvican left a comment

Choose a reason for hiding this comment

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

LGTM, this change is for the better. 👍

@SethTisue
Copy link
Member

Deprecating in 2.12.7 and removing entirely in 2.13.0-RC1 might not be out of the question. wdyt?

@ceedubs
Copy link
Contributor Author

ceedubs commented Sep 3, 2018

@SethTisue sounds great to me. Who is the "y" in wdyt? :)

@milessabin
Copy link
Contributor

I'm 100% in favour of removing the instance entirely in 2.13.0-RC1.

I'm not so sure about deprecation, in any release. If something is deprecated there has to be some feasible remedial action the receiver of the deprecation message can take. In this case I don't see what that could be, other than "put up with it until you can migrate to 2.13.0".

@martijnhoekstra
Copy link
Contributor

If something is deprecated there has to be some feasible remedial action the receiver of the deprecation message can take.

And there is -- they can use Equiv.universal instead, just like the issued warning indicates.

Of course, maybe they don't want to use it at all, rather than explicitly through Equiv.universal. In that case, they now get a deprecation warning message, so at least they're made aware.

@milessabin
Copy link
Contributor

And there is -- they can use Equiv.universal instead, just like the issued warning indicates.

This makes perfectly good sense for explicitly applied methods, but not for implicitly applied methods.

An end user will not typically be using either explicitly. To use Equiv.universal they will have to rewrite some or all of an implicit expansion explicitly which is at best ugly and can in general be extremely difficult.

@SethTisue
Copy link
Member

could we provide a universal instance that isn't deprecated, but isn't found by implicit search unless it is explicitly imported?

@hrhino
Copy link
Contributor

hrhino commented Sep 4, 2018

To use Equiv.universal they will have to rewrite some or all of an implicit expansion explicitly

surely the hypothetical user can just have a local implicit val univQ: Equiv[Q] = Equiv.universal, specifying the type for which they want this notion of equivalence.

I'm for deprecating this in 2.12.7 and having it completely gone in 2.13.0, by the way.

@SethTisue
Copy link
Member

SethTisue commented Sep 4, 2018

surely the hypothetical user can just have a local implicit val univQ: Equiv[Q] = Equiv.universal, specifying the type for which they want this notion of equivalence

that's less convenient than providing something importable, since import can have whole-source-file scope but a val can't. and users generally expect to use import to get an implicit

not saying it's a big deal either way, just explaining

@milessabin
Copy link
Contributor

surely the hypothetical user can just have a local implicit val univQ: Equiv[Q] = Equiv.universal, specifying the type for which they want this notion of equivalence.

Yes, if you know what Q is. In many cases that will be clear, but in many it won't. Imagine Q is a complex inferred type buried deep in a nested implicit resolution.

@milessabin
Copy link
Contributor

A github search suggests that Equiv is hardly used at all, so in practical terms I guess deprecation warnings aren't that big a deal.

@NthPortal
Copy link
Contributor

I would be in favour of putting it inside an Implicits (or Universal?) object so that it's not available by default, but still exists as an implicit method

@ceedubs
Copy link
Contributor Author

ceedubs commented Sep 5, 2018

Everyone seems to be in favor of removing this in its current form for Scala 2.13. Yay 🎉 I can do that.

Now for my thoughts/observations on the more controversial pieces.

Miles' GitHub search lines up with my experience that Equiv is rarely used, so the good news is that no matter what we do, we probably aren't breaking much code.

If someone were to want to recreate this, they would need to do something like the following (and then import it):

object Implicits {
  implicit def universalEquiv[A]: Equiv[A] = Equiv.universal
}

There is some talk of adding something like this to the standard library. My initial reaction is hesitation to do this, because of a combination of the two following reasons:

  • It's generally agreed that implicit universal equality is error-prone. I don't want to lead people down a path of seeing this and thinking that it's a good solution to their problem. While there's some chance that we make a few peoples' lives easier by adding this, I think that it's equally or more likely that we make some peoples' lives harder by leading them down the wrong path.
  • If someone really still wants this functionality despite the problems (which is okay), the above chunk of code is concise and simple enough that to me it doesn't seem like much of a burden for them to add the above snippet to their code. Again, I expect this to be very rare (if not nonexistent) based on the limited usage of Equiv and the problems with universal Equiv.

I've mentored a number of people in Scala and a recurring pattern that I see is people new to Scala seeing stuff in the standard library (such as break/breakout -- often auto-complete suggestions from their IDE) and assuming that means that it's the idiomatic scala way to approach what they are trying to do. I'm hesitant to add something to the standard library that seems like it could lead down this path when we don't even know of a concrete case of someone wanting this. It feels a bit DOA (Deprecated-On-Arrival).

@jvican
Copy link
Member

jvican commented Sep 7, 2018

@ceedubs I agree with your analysis, thanks for writing that up 👍

To make the deprecation warning more actionable, could we describe the migration strategy in the deprecation message? Agreed that it can be long, but I think it's worth it 😄 In the message, I would also invite users to think if they really need universal equality in Equiv as in most of the cases the warning could surface a bug in their code.

@milessabin
Copy link
Contributor

milessabin commented Sep 7, 2018

I still object to the deprecation on the LowPriorityEquiv trait declaration.

LowPriorityEquiv can't be used anywhere other than as a mixin of the Equiv object thanks to its self type. As such the deprecation is just adding unfixable noise to the compiler and standard library builds.

That's a negative with no upside that I can see.

@jvican
Copy link
Member

jvican commented Sep 7, 2018

LowPriorityEquiva can't be used anywhere other than as a mixin of the Equiv object thanks to its self type. As such the deprecation is just adding unfixable noise to the compiler and standard library builds.

I think it's appropriate to ask users that really need the type to be inferred to inline the implicit definition in an object ehy own or, if they really want it enabled by default, in a package object (which in 2.12.x is still an implicit candidate). I wouldn't worry about LowPriorityEquiv not being reusable downstream.

@milessabin
Copy link
Contributor

I wouldn't worry about LowPriorityEquiv not being reusable downstream.

I'm not worrying about that. I'm worrying about the fact that the deprecation warning on the trait will bug everyone building the scala/scala tree to no useful end. The fact that the trait isn't reusable means that these are the only people who will ever see the deprecation.

def equiv(x: T, y: T): Boolean
}

@deprecated("This only existed for the implicit universalEquiv. Use the explicit Equiv.universal instead.", "2.13.0")
Copy link
Member

Choose a reason for hiding this comment

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

As Miles says, given the self-type, this isn't useable anywhere outside of Equiv, so let's avoid the spam.

@ceedubs
Copy link
Contributor Author

ceedubs commented Sep 7, 2018

Oh I was missing the bit about the self type making it so that nobody else could have been extending the trait. That makes sense.

Thanks for all of the good feedback, everyone. I'll close this out and soon I'll create 2 new PRs: one to remove these for 2.13 and one to deprecate the instance (with a more helpful error message) in 2.12.

@ceedubs ceedubs closed this Sep 7, 2018
ceedubs added a commit to ceedubs/scala that referenced this pull request Sep 7, 2018
This targets the 2.12 branch. See discussion [here](scala#7164).
ceedubs added a commit to ceedubs/scala that referenced this pull request Sep 7, 2018
Based on discussion [here](scala#7164).

I didn't seem to have to change any tests for this, so either it was
untested or I'm missing something.
ceedubs added a commit to ceedubs/scala that referenced this pull request Sep 7, 2018
Based on discussion [here](scala#7164).

I didn't seem to have to change any tests for this, so either it was
untested or I'm missing something.
ceedubs added a commit to ceedubs/scala that referenced this pull request Sep 7, 2018
This targets the 2.12 branch. See discussion [here](scala#7164).
@SethTisue SethTisue removed this from the 2.13.0-RC1 milestone Feb 22, 2019
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.

9 participants