Skip to content

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 10 commits into from
Feb 12, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Mode.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ object Mode {
/** Allow GADTFlexType labelled types to have their bounds adjusted */
val GADTflexible: Mode = newMode(8, "GADTflexible")

/** Assume -language:strictEquality */
val StrictEquality: Mode = newMode(9, "StrictEquality")

/** We are currently printing something: avoid to produce more logs about
* the printing
*/
Expand Down
164 changes: 101 additions & 63 deletions docs/docs/reference/other-new-features/multiversal-equality.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

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...

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]`
Copy link
Contributor

Choose a reason for hiding this comment

The 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).
53 changes: 14 additions & 39 deletions library/src/scala/Eq.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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
}
8 changes: 4 additions & 4 deletions tests/neg/equality.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ object equality {

1 == true // error

null == true // OK by eqProxy or eqJBoolSBool
true == null // OK by eqSBoolJBool
null == 1 // OK by eqProxy or eqNumInt
1 == null // OK by eqIntNum
null == true // error
true == null // error
null == 1 // error
1 == null // error


class Fruit derives Eq
Expand Down
30 changes: 30 additions & 0 deletions tests/pos/multiversal.scala
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 _ =>
}
}
}
2 changes: 1 addition & 1 deletion tests/run/lst/Lst.scala
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ object Lst {
def fromIterable[T](xs: Iterable[T]): Lst[T] = fromIterator(xs.iterator)

object :: {
def unapply[T](xs: Lst[T]): Option[(T, Lst[T])] = xs match {
def unapply[T](xs: Lst[T]): Option[(T, Lst[T])] = xs.elems match {
case null => None
case elems: Arr =>
Some((elems(0).asInstanceOf[T], _fromArray[T](elems, 1, elems.length)))
Expand Down