scalabook

Форк
0
/
ca-multiversal-equality.md 
202 строки · 8.7 Кб

Многостороннее равенство

Раньше в Scala было универсальное равенство (universal equality): два значения любых типов можно было сравнивать друг с другом с помощью ==

и !=
. Это произошло из-за того факта, что ==
и !=
реализованы в терминах метода equals
Java, который также может сравнивать значения любых двух ссылочных типов.

Всеобщее равенство удобно, но оно также опасно, поскольку подрывает безопасность типов. Например, предположим, что после некоторого рефакторинга осталась ошибочная программа, в которой значение y

имеет тип S
вместо правильного типа T
:

val x = ... // типа T
val y = ... // типа S, но должно быть типа T
x == y // проверки типов всегда будут выдавать false

Если y

сравнивается с другими значениями типа T
, программа все равно будет проверять тип, так как значения всех типов можно сравнивать друг с другом. Но это, вероятно, даст неожиданные результаты и завершится ошибкой во время выполнения.

Типобезопасный язык программирования может работать лучше, а мультиуниверсальное равенство — это дополнительный способ сделать универсальное равенство более безопасным. Он использует класс двоичного типа CanEqual

, чтобы указать, что значения двух заданных типов можно сравнивать друг с другом.

Разрешение сравнения экземпляров класса

По умолчанию сравнение на равенство можно создать следующим образом:

case class Cat(name: String)
case class Dog(name: String)
val d = Dog("Fido")
val c = Cat("Morris")
d == c // false, но он компилируется

Но в Scala 3 такие сравнения можно отключить. При (а) импорте scala.language.strictEquality

или (б) использовании флага компилятора -language:strictEquality
это сравнение больше не компилируется:

import scala.language.strictEquality
val rover = Dog("Rover")
val fido = Dog("Fido")
println(rover == fido) // compiler error
// compiler error message:
// Values of types Dog and Dog cannot be compared with == or !=

Включение сравнений

Есть два способа включить сравнение с помощью класса типов CanEqual

. Для простых случаев класс может выводиться (derive) от класса CanEqual
:

// Способ 1
case class Dog(name: String) derives CanEqual

Также можно использовать следующий синтаксис:

// Способ 2
case class Dog(name: String)
given CanEqual[Dog, Dog] = CanEqual.derived

Любой из этих двух подходов позволяет сравнивать экземпляры Dog

друг с другом.

Более реалистичный пример

В более реалистичном примере представим, что есть книжный интернет-магазин и мы хотим разрешить или запретить сравнение бумажных, печатных и аудиокниг. В Scala 3 для начала необходимо включить мультиуниверсальное равенство:

// [1] добавить этот импорт или command line flag: -language:strictEquality
import scala.language.strictEquality

Затем создать объекты домена:

// [2] создание иерархии классов
trait Book:
def author: String
def title: String
def year: Int
case class PrintedBook(
author: String,
title: String,
year: Int,
pages: Int
) extends Book
case class AudioBook(
author: String,
title: String,
year: Int,
lengthInMinutes: Int
) extends Book

Наконец, используем CanEqual

, чтобы определить, какие сравнения необходимо разрешить:

// [3] создайте экземпляры класса типов, чтобы определить разрешенные сравнения.
// разрешено `PrintedBook == PrintedBook`
// разрешено `AudioBook == AudioBook`
given CanEqual[PrintedBook, PrintedBook] = CanEqual.derived
given CanEqual[AudioBook, AudioBook] = CanEqual.derived
// [4a] сравнение двух печатных книг разрешено
val p1 = PrintedBook("1984", "George Orwell", 1961, 328)
val p2 = PrintedBook("1984", "George Orwell", 1961, 328)
println(p1 == p2) // true
// [4b] нельзя сравнивать печатную книгу и аудиокнигу
val pBook = PrintedBook("1984", "George Orwell", 1961, 328)
val aBook = AudioBook("1984", "George Orwell", 2006, 682)
println(pBook == aBook) // compiler error

Последняя строка кода приводит к следующему сообщению компилятора об ошибке:

Values of types PrintedBook and AudioBook cannot be compared with == or !=

Вот как мультиуниверсальное равенство отлавливает недопустимые сравнения типов во время компиляции.

Включение «PrintedBook == AudioBook»

Если есть необходимость разрешить сравнение PrintedBook

с AudioBook
, то достаточно создать следующие два дополнительных сравнения равенства:

// разрешить `PrintedBook == AudioBook` и `AudioBook == PrintedBook`
given CanEqual[PrintedBook, AudioBook] = CanEqual.derived
given CanEqual[AudioBook, PrintedBook] = CanEqual.derived

Теперь можно сравнивать PrintedBook

с AudioBook
без ошибки компилятора:

println(pBook == aBook) // false
println(aBook == pBook) // false
Внедрение «equals»

Хотя эти сравнения теперь разрешены, они всегда будут ложными, потому что их методы equals

не знают, как проводить подобные сравнения. Чтобы доработать сравнение, можно переопределить методы equals
для каждого класса. Например, если переопределить метод equals
для AudioBook
:

case class AudioBook(
author: String,
title: String,
year: Int,
lengthInMinutes: Int
) extends Book:
// переопределить, чтобы разрешить сравнение AudioBook с PrintedBook
override def equals(that: Any): Boolean = that match
case a: AudioBook =>
if this.author == a.author
&& this.title == a.title
&& this.year == a.year
&& this.lengthInMinutes == a.lengthInMinutes
then true else false
case p: PrintedBook =>
if this.author == p.author && this.title == p.title
then true else false
case _ =>
false

Теперь можно сравнить AudioBook

с PrintedBook
:

println(aBook == pBook) // true (работает из-за переопределенного `equals` в `AudioBook`)
println(pBook == aBook) // false

Книга PrintedBook

не имеет метода equals
, поэтому второе сравнение возвращает false
. Чтобы включить это сравнение, достаточно переопределить метод equals
в PrintedBook
.


Ссылки:

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.