-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Base multiversal equality on typeclass derivation #5843
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
Merged
Merged
Changes from 1 commit
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
bfcb4c6
Make Eq a derivable typeclass
odersky eb1f5f3
Drop generation of Eq instances for enums
odersky 941e697
Use `Eq.derived` as the universal Eq instance
odersky 1d08aa1
Change rules for multiversal equality
odersky c45bd8a
Change rules for multiversal equality
odersky 996f6a6
Fix neg tests
odersky 7c49b44
Move equality reference doc under contextual abstractions
odersky a9ef28c
Rename Eq -> Eql
odersky f791a52
Avoid duplicating parameter names
odersky 4bc1514
update scalatest
odersky File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,112 +9,150 @@ the fact that `==` and `!=` are implemented in terms of Java's | |
`equals` method, which can also compare values of any two reference | ||
types. | ||
|
||
Universal equality is convenient but also dangerous since it | ||
undermines type safety. Say you have an erroneous program where | ||
a value `y` has type `S` instead of the expected type `T`. | ||
Universal equality is convenient. But it is also dangerous since it | ||
undermines type safety. For instance, let's assume one is left after some refactoring | ||
with an erroneous program where a value `y` has type `S` instead of the correct type `T`. | ||
|
||
```scala | ||
val x = ... // of type T | ||
val y = ... // of type S, but should be T | ||
x == y // typechecks, will always yield false | ||
``` | ||
|
||
If all you do with `y` is compare it to other values of type `T`, the program will | ||
typecheck but probably give unexpected results. | ||
If all the program does with `y` is compare it to other values of type `T`, the program will still typecheck, since values of all types can be compared with each other. | ||
But it will probably give unexpected results and fail at runtime. | ||
|
||
Multiversal equality is an opt-in way to make universal equality | ||
safer. The idea is that by declaring an `implicit` value one can | ||
restrict the types that are legal in comparisons. The example above | ||
would not typecheck if an implicit was declared like this for type `T` | ||
(or an analogous one for type `S`): | ||
|
||
safer. It uses a binary typeclass `Eql` to indicate that values of | ||
two given types can be compared with each other. | ||
The example above would not typecheck if `S` or `T` was a class | ||
that derives `Eql`, e.g. | ||
```scala | ||
implicit def eqT: Eq[T, T] = Eq.derived | ||
class T derives Eql | ||
``` | ||
|
||
This definition effectively says that value of type `T` can (only) be | ||
compared with `==` or `!=` to other values of type `T`. The definition | ||
is used only for type checking; it has no significance for runtime | ||
Alternatively, one can also provide the derived implied instance directly, like this: | ||
```scala | ||
implied for Eql[T, T] = Eql.derived | ||
``` | ||
This definition effectively says that values of type `T` can (only) be | ||
compared to other values of type `T` when using `==` or `!=`. The definition | ||
affects type checking but it has no significance for runtime | ||
behavior, since `==` always maps to `equals` and `!=` always maps to | ||
the negation of `equals`. The right hand side of the definition is a value | ||
that has any `Eq` instance as its type. Here is the definition of class | ||
`Eq` and its companion object: | ||
|
||
the negation of `equals`. The right hand side `Eql.derived` of the definition | ||
is a value that has any `Eql` instance as its type. Here is the definition of class | ||
`Eql` and its companion object: | ||
```scala | ||
package scala | ||
import annotation.implicitNotFound | ||
|
||
@implicitNotFound("Values of types ${L} and ${R} cannot be compared with == or !=") | ||
sealed trait Eq[-L, -R] | ||
sealed trait Eql[-L, -R] | ||
|
||
object Eq extends Eq[Any, Any] | ||
object Eql { | ||
object derived extends Eql[Any, Any] | ||
} | ||
``` | ||
|
||
One can have several `Eq` instances for a type. For example, the four | ||
One can have several `Eql` instances for a type. For example, the four | ||
definitions below make values of type `A` and type `B` comparable with | ||
each other, but not comparable to anything else: | ||
|
||
```scala | ||
implicit def eqA : Eq[A, A] = Eq.derived | ||
implicit def eqB : Eq[B, B] = Eq.derived | ||
implicit def eqAB: Eq[A, B] = Eq.derived | ||
implicit def eqBA: Eq[B, A] = Eq.derived | ||
implied for Eql[A, A] = Eql.derived | ||
implied for Eql[B, B] = Eql.derived | ||
implied for Eql[A, B] = Eql.derived | ||
implied for Eql[B, A] = Eql.derived | ||
``` | ||
The `scala.Eql` object defines a number of `Eql` instances that together | ||
define a rule book for what standard types can be compared (more details below). | ||
|
||
(As usual, the names of the implicit definitions don't matter, we have | ||
chosen `eqA`, ..., `eqBA` only for illustration). | ||
|
||
The `scala.Eq` object defines a number of `Eq` implicits that make | ||
values of types `String`, `Boolean` and `Unit` only comparable to | ||
values of the same type. They also make numbers only comparable to | ||
other numbers, sequences only comparable to other | ||
sequences and sets only comparable to other sets. | ||
|
||
There's also a "fallback" instance named `eqAny` that allows comparisons | ||
over all types that do not themselves have an `Eq` instance. `eqAny` is | ||
There's also a "fallback" instance named `eqlAny` that allows comparisons | ||
over all types that do not themselves have an `Eql` instance. `eqlAny` is | ||
defined as follows: | ||
|
||
```scala | ||
def eqAny[L, R]: Eq[L, R] = Eq.derived | ||
def eqlAny[L, R]: Eql[L, R] = Eql.derived | ||
``` | ||
|
||
Even though `eqAny` is not declared implicit, the compiler will still | ||
construct an `eqAny` instance as answer to an implicit search for the | ||
type `Eq[L, R]`, provided that neither `L` nor `R` have `Eq` instances | ||
defined on them. | ||
Even though `eqlAny` is not declared `implied`, the compiler will still | ||
construct an `eqlAny` instance as answer to an implicit search for the | ||
type `Eql[L, R]`, unless `L` or `R` have `Eql` instances | ||
defined on them, or the language feature `strictEquality` is enabled | ||
|
||
The primary motivation for having `eqAny` is backwards compatibility, | ||
if this is of no concern one can disable `eqAny` by enabling the language | ||
The primary motivation for having `eqlAny` is backwards compatibility, | ||
if this is of no concern one can disable `eqlAny` by enabling the language | ||
feature `strictEquality`. As for all language features this can be either | ||
done with an import | ||
|
||
```scala | ||
import scala.language.strictEquality | ||
``` | ||
|
||
or with a command line option `-language:strictEquality`. | ||
|
||
All `enum` types also come with `Eq` instances that make values of the | ||
`enum` type comparable only to other values of that `enum` type. | ||
## Deriving Eql Instances | ||
|
||
Instead of defining `Eql` instances directly, it is often more convenient to derive them. Example: | ||
```scala | ||
class Box[T](x: T) derives Eql | ||
``` | ||
By the usual rules if [typeclass derivation](./derivation.html), | ||
this generates the following `Eql` instance in the companion object of `Box`: | ||
```scala | ||
implied [T, U] given Eql[T, U] for Eql[Box[T], Box[U]] = Eql.derived | ||
``` | ||
That is, two boxes are comparable with `==` or `!=` if their elements are. Examples: | ||
```scala | ||
new Box(1) == new Box(1L) // ok since `Eql[Int, Long]` is an implied instance | ||
new Box(1) == new Box("a") // error: can't compare | ||
new Box(1) == 1 // error: can't compare | ||
``` | ||
|
||
## Precise Rules for Equality Checking | ||
|
||
The precise rules for equality checking are as follows. | ||
|
||
1. A comparison using `x == y` or `x != y` between values `x: T` and `y: U` | ||
is legal if either `T` and `U` are the same, or one of the types is a subtype | ||
of the "lifted" version of the other type, or an implicit value of type `scala.Eq[T, U]` is found. | ||
See the [description on Github](https://github.com/lampepfl/dotty/issues/1247) for | ||
a definition of lifting. | ||
|
||
2. The usual rules for implicit search apply also to `Eq` instances, | ||
with one modification: If the `strictEquality` feature is not enabled, | ||
an instance of `scala.Eq.eqAny[T, U]` is constructed if neither `T` | ||
nor `U` have a reflexive `Eq` instance themselves. Here, a type `T` | ||
has a reflexive `Eq` instance if the implicit search for `Eq[T, T]` | ||
succeeds and constructs an instance different from `eqAny`. | ||
|
||
Here _lifting_ a type `S` means replacing all references to abstract types | ||
in covariant positions of `S` by their upper bound, and to replacing | ||
all refinement types in covariant positions of `S` by their parent. | ||
If the `strictEquality` feature is enabled then | ||
a comparison using `x == y` or `x != y` between values `x: T` and `y: U` | ||
is legal if | ||
|
||
1. there is an implied instance of type `Eql[T, U]`, or | ||
2. one of `T`, `U` is `Null`. | ||
|
||
In the default case where the `strictEquality` feature is not enabled the comparison is | ||
also legal if | ||
|
||
1. `T` and `U` the same, or | ||
2. one of `T` and `U`is a subtype of the _lifted_ version of the other type, or | ||
3. neither `T` nor `U` have a _reflexive `Eql` instance_. | ||
|
||
Explanations: | ||
|
||
- _lifting_ a type `S` means replacing all references to abstract types | ||
in covariant positions of `S` by their upper bound, and to replacing | ||
all refinement types in covariant positions of `S` by their parent. | ||
- a type `T` has a _reflexive `Eql` instance_ if the implicit search for `Eql[T, T]` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment (on implicit search) |
||
succeeds. | ||
|
||
## Predefined Eql Instances | ||
|
||
The `Eql` object defines implied instances for | ||
- the primitive types `Byte`, `Short`, `Char`, `Int`, `Long`, `Float`, `Double`, `Boolean`, and `Unit`, | ||
- `java.lang.Number`, `java.lang.Boolean`, and `java.lang.Character`, | ||
- `scala.collection.Seq`, and `scala.collection.Set`. | ||
|
||
Implied instances are defined so that everyone of these types is has a reflexive `Eql` instance, and the following holds: | ||
|
||
- Primitive numeric types can be compared with each other. | ||
- Primitive numeric types can be compared with subtypes of `java.lang.Number` (and _vice versa_). | ||
- `Boolean` can be compared with `java.lang.Boolean` (and _vice versa_). | ||
- `Char` can be compared with `java.lang.Character` (and _vice versa_). | ||
- Two sequences (of arbitrary subtypes of `scala.collection.Seq`) can be compared | ||
with each other if their element types can be compared. The two sequence types | ||
need not be the same. | ||
- Two sets (of arbitrary subtypes of `scala.collection.Set`) can be compared | ||
with each other if their element types can be compared. The two set types | ||
need not be the same. | ||
- Any subtype of `AnyRef` can be compared with `Null` (and _vice versa_). | ||
|
||
More on multiversal equality is found in a [blog post](http://www.scala-lang.org/blog/2016/05/06/multiversal-equality.html) | ||
and a [Github issue](https://github.com/lampepfl/dotty/issues/1247). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,56 +7,31 @@ import scala.collection.{GenSeq, Set} | |
@implicitNotFound("Values of types ${L} and ${R} cannot be compared with == or !=") | ||
sealed trait Eq[-L, -R] | ||
|
||
/** Besides being a companion object, this object | ||
* can also be used as a value that's compatible with | ||
* any instance of `Eq`. | ||
/** Companion object containing a few universally known `Eql` instances. | ||
* Eql instances involving primitive types or the Null type are handled directly in | ||
* the compiler (see Implicits.synthesizedEq), so they are not included here. | ||
*/ | ||
object Eq { | ||
/** A non-implied universal `Eql` instance. */ | ||
object derived extends Eq[Any, Any] | ||
|
||
/** A fall-back "implicit" to compare values of any types. | ||
* Even though this method is not declared implicit, the compiler will | ||
* compute instances as solutions to `Eq[T, U]` queries if `T <: U` or `U <: T` | ||
* or both `T` and `U` are Eq-free. A type `S` is Eq-free if there is no | ||
* implicit instance of type `Eq[S, S]`. | ||
/** A fall-back instance to compare values of any types. | ||
* Even though this method is not declared implied, the compiler will | ||
* compute implied instances as solutions to `Eql[T, U]` queries if | ||
* the rules of multiversal equality require it. | ||
*/ | ||
def eqAny[L, R]: Eq[L, R] = derived | ||
|
||
// Instances of `Eq` for common types | ||
|
||
// Instances of `Eq` for common Java types | ||
implicit def eqNumber : Eq[Number, Number] = derived | ||
implicit def eqString : Eq[String, String] = derived | ||
implicit def eqBoolean : Eq[Boolean, Boolean] = derived | ||
implicit def eqByte : Eq[Byte, Byte] = derived | ||
implicit def eqShort : Eq[Short, Short] = derived | ||
implicit def eqChar : Eq[Char, Char] = derived | ||
implicit def eqInt : Eq[Int, Int] = derived | ||
implicit def eqLong : Eq[Long, Long] = derived | ||
implicit def eqFloat : Eq[Float, Float] = derived | ||
implicit def eqDouble : Eq[Double, Double] = derived | ||
implicit def eqUnit : Eq[Unit, Unit] = derived | ||
|
||
// true asymmetry, modeling the (somewhat problematic) nature of equals on Proxies | ||
implicit def eqProxy : Eq[Proxy, Any] = derived | ||
|
||
// The next three definitions can go into the companion objects of classes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, but then maybe then spec shouldn't mention them... |
||
// Seq, Set, and Proxy. For now they are here in order not to have to touch the | ||
// source code of these classes | ||
implicit def eqSeq[T, U](implicit eq: Eq[T, U]): Eq[GenSeq[T], GenSeq[U]] = derived | ||
implicit def eqSet[T, U](implicit eq: Eq[T, U]): Eq[Set[T], Set[U]] = derived | ||
|
||
implicit def eqByteNum : Eq[Byte, Number] = derived | ||
implicit def eqNumByte : Eq[Number, Byte] = derived | ||
implicit def eqCharNum : Eq[Char, Number] = derived | ||
implicit def eqNumChar : Eq[Number, Char] = derived | ||
implicit def eqShortNum : Eq[Short, Number] = derived | ||
implicit def eqNumShort : Eq[Number, Short] = derived | ||
implicit def eqIntNum : Eq[Int, Number] = derived | ||
implicit def eqNumInt : Eq[Number, Int] = derived | ||
implicit def eqLongNum : Eq[Long, Number] = derived | ||
implicit def eqNumLong : Eq[Number, Long] = derived | ||
implicit def eqFloatNum : Eq[Float, Number] = derived | ||
implicit def eqNumFloat : Eq[Number, Float] = derived | ||
implicit def eqDoubleNum: Eq[Double, Number] = derived | ||
implicit def eqNumDouble: Eq[Number, Double] = derived | ||
|
||
implicit def eqSBoolJBool: Eq[Boolean, java.lang.Boolean] = derived | ||
implicit def eqJBoolSBool: Eq[java.lang.Boolean, Boolean] = derived | ||
// true asymmetry, modeling the (somewhat problematic) nature of equals on Proxies | ||
implicit def eqProxy : Eq[Proxy, AnyRef] = derived | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
object Test { | ||
import scala.Eq | ||
|
||
implied [X, Y] given Eq[X, Y] for Eq[List[X], List[Y]] = Eq.derived | ||
|
||
val b: Byte = 1 | ||
val c: Char = 2 | ||
val i: Int = 3 | ||
val l: Long = 4L | ||
val ii: Integer = i | ||
|
||
List(b) == List(l) | ||
List(l) == List(c) | ||
List(b) != List(c) | ||
List(i) == List(l) | ||
List(i) == List(ii) | ||
List(ii) == List(l) | ||
List(b) == List(ii) | ||
List(ii) == List(l) | ||
|
||
import reflect.ClassTag | ||
val BooleanTag: ClassTag[Boolean] = ClassTag.Boolean | ||
|
||
class Setting[T: ClassTag] { | ||
def doSet() = implicitly[ClassTag[T]] match { | ||
case BooleanTag => | ||
case _ => | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there also an alternative name for implicit search? Mixing the two terminologies here makes the sentence a bit confusing...