Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 786
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 22.5. Вызов метода расширения того же уровня extension (n: Int)
def isMinValue: Boolean = n == Int.MinValue def absOption: Option[Int] =
if !isMinValue then Some(n.abs) else None def negateOption: Option[Int] =
if !isMinValue then Some(-n) else None
В групповом расширении, показанном в листинге 22.5, метод isMinValue вызывается как из absOption
, так и из negateOption
. В таких случаях компи
лятор переопределит вызов так, чтобы он выполнялся из получателя. В дан
ном расширении, к примеру, компилятор подставит n.isMinValue вместо isMinValue
, как показано в листинге 22.6.
Листинг 22.6. Групповое расширение, после переопределения компилятором
// Все с внутренними обозначениями расширения def isMinValue(n: Int): Boolean = n == Int.MinValue def absOption(n: Int): Option[Int] =
if !n.isMinValue then Some(n.abs) else None def negateOption(n: Int): Option[Int] =
if !n.isMinValue then Some(-n) else None
22 .4 . Использование класса типов
Распознавание переполнения в операциях с получением абсолютного зна
чения и изменением знака на противоположный имеет смысл не только для типа
Int
. Любому целочисленному типу, основанному на арифметике в дополнительном коде, свойственна одна и та же проблема с переполне
нием:
22 .4 . Использование класса типов 475
Long.MinValue.abs // -9223372036854775808 (переполнение)
-Long.MinValue // -9223372036854775808 (переполнение)
Short.MinValue.abs // -32768 (переполнение)
-Short.MinValue // -32768 (переполнение)
Byte.MinValue.abs // -128 (переполнение)
-Byte.MinValue // -128 (переполнение)
Если вам нужны безопасные альтернативы abs и unary_- для всех этих ти
пов, вы можете определить отдельное групповое расширение для каждого из них, но в этом случае все реализации будут выглядеть одинаково. Чтобы не дублировать код, вы можете определить вместо этого расширение на основе класса типов. Такое специализированное расширение будет работать для лю
бого типа с givenэкземпляром класса типа.
Чтобы проверить, существует ли трейт с подходящим классом типов, стоит заглянуть в стандартную библиотеку. Трейт
Numeric слишком общий, так как givenэкземпляры предоставляются для типов вроде
Double или
Float
, которые не основаны на арифметике в дополнительном коде. То же самое можно сказать о трейте
Integral
, только вместо
Double или
Float given
экземпляр предоставлен для типа
BigInt
, который не переполняется. Таким образом, самый оптимальный вариант состоит в определении нового трейта специально для целочисленных типов в дополнительном коде, как, например, трейт
TwosComplement
, показанный в листинге 22.7.
После этого следует определить givenэкземпляры для типов в дополни
тельном коде, которые будут содержать методы расширения. Подходящим местом их размещения будет объекткомпаньон, доступ к которому, как вы ожидаете, всегда будет нужен пользователям
1
. В листинге 22.7 given
экземпляры
TwosComplement определены для
Byte
,
Short
,
Int и
Long
Листинг 22.7. Класс типов для чисел в дополнительном коде trait TwosComplement[N]:
def equalsMinValue(n: N): Boolean def absOf(n: N): N
def negationOf(n: N): N
object TwosComplement:
given tcOfByte: TwosComplement[Byte] with def equalsMinValue(n: Byte) = n == Byte.MinValue def absOf(n: Byte) = n.abs def negationOf(n: Byte) = (-n).toByte
1
Совет о том, где лучше определять givenэкземпляры, был дан в разделе 21.5.
476 Глава 22 • Методы расширения given tcOfShort: TwosComplement[Short] with def equalsMinValue(n: Short) = n == Short.MinValue def absOf(n: Short) = n.abs def negationOf(n: Short) = (-n).toShort given tcOfInt: TwosComplement[Int] with def equalsMinValue(n: Int) = n == Int.MinValue def absOf(n: Int) = n.abs def negationOf(n: Int) = -n given tcOfLong: TwosComplement[Long] with def equalsMinValue(n: Long) = n == Long.MinValue def absOf(n: Long) = n.abs def negationOf(n: Long) = -n
Имея в своем распоряжении эти определения, вы можете написать обобщенный метод расширения, как показано в листинге 22.8. Это позволит использовать absOption и negateOption для подходящих типов.
Листинг 22.8. Использование класса типов в расширении
Byte.MaxValue.negateOption // Some(-127)
Byte.MinValue.negateOption // None
Long.MaxValue.negateOption // -9223372036854775807
Long.MinValue.negateOption // None extension [N](n: N)(using tc: TwosComplement[N])
def isMinValue: Boolean = tc.equalsMinValue(n)
def absOption: Option[N] =
if !isMinValue then Some(tc.absOf(n)) else None def negateOption: Option[N] =
if !isMinValue then Some(tc.negationOf(n)) else None
С другой стороны, любая попытка использования этих методов расширения для неподходящих типов приведет к ошибке компиляции:
BigInt(42).negateOption
1 |BigInt(42).negateOption
|ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
|value negateOption is not a member of BigInt.
|An extension method was tried, but could not be
|fully constructed:
|
| negateOption[BigInt](BigInt.apply(42))(
| /* missing */summon[TwosComplement[BigInt]]
| )
Как уже обсуждалось в разделе 21.4, классы типов обеспечивают специаль
ный полиморфизм: функционал, доступный только для определенных типов
(тех, для которых существуют givenэкземпляры класса типов) и выдающий
22 .5 . Методы расширения для гивенов 477
ошибку компиляции для любого другого типа. Классы типов можно исполь
зовать для получения синтаксического сахара в виде методов расширения для определенных типов. Любые попытки применения методов расширения к другим типам не позволят скомпилировать код.
22 .5 . Методы расширения для гивенов
В предыдущем разделе задача класса типов
TwosComplement состояла в пре
доставлении методов расширения для определенного множества типов.
Поскольку основным потребителем методов расширения является поль
зователь, у него не должно вызывать затруднений решение о том, когда их следует делать доступными и стоит ли это делать в принципе. В таких ситуациях расширение лучше всего размещать в объектеодиночке. Ваши пользователи могут импортировать методы расширения из этого объекта в лексическую область видимости, что сделает возможным их применение.
Вы можете, к примеру, поместить групповое расширение для распозна
вания переполнений в объект с именем
TwosComplementOps
, как показано в листинге 22.9.
Листинг 22.9. Размещение методов расширения в объекте-одиночке object TwosComplementOps:
extension [N](n: N)(using tc: TwosComplement[N])
def isMinValue: Boolean = tc.equalsMinValue(n)
def absOption: Option[N] =
if !isMinValue then Some(tc.absOf(n)) else None def negateOption: Option[N] =
if !isMinValue then Some(tc.negationOf(n)) else None
Затем ваши пользователи могут добавить в свой код немного синтаксиче
ского сахара:
import TwosComplementOps.*
Благодаря этому импорту методы расширения будут доступны для при
менения:
-42.absOption // Some(42)
В случае с
TwosComplementOps методы расширения представляют основную цель проектирования, а класс типов играет вспомогательную роль. Но зача
стую все наоборот: класс типов служит главной целью, а методы расширения помогают упростить использование этого класса. В таких ситуациях методы расширения лучше всего размещать в трейте самого класса типов.
478 Глава 22 • Методы расширения
Например, в главе 21 класс типов
Ord был определен, чтобы сделать метод сортировки вставками, isort
, более общим. И хотя эта цель была достигнута с помощью решений, представленных в главе 21 (метод isort можно исполь
зовать с любым типом
T
, для которого доступен givenэкземпляр
Ord[T]
), добавление нескольких методов расширения сделает класс типов
Ord более приятным в использовании.
Каждый трейт с классом типов принимает параметр, поскольку экземпляр этого класса знает, как обращаться с объектами этого типа. Например,
Ord[T]
знает, как сравнивать два экземпляра типа
T
для определения того, какой из них больше или равен другому. Поскольку экземпляр класса типов для
T
— не то же самое, что экземпляр или экземпляры
T
, синтаксис использования классов типов может быть немного громоздким. Например, в листинге 21.1 метод insert принимает givenэкземпляр
Ord[T]
и определяет с его помощью, является ли экземпляр
T
равным начальному элементу уже отсортированного списка или меньше его. Вот как выглядит метод insert из этого листинга:
def insert[T](x: T, xs: List[T])(using ord: Ord[T]): List[T] =
if xs.isEmpty || ord.lteq(x, xs.head) then x :: xs else xs.head :: insert(x, xs.tail)
Вызов ord.lteq(x,
xs.head)
является вполне нормальным, но его можно было бы записать более естественным и, наверное, более понятным образом:
x <= xs.head // Теперь намного понятнее!
Синтаксический сахар
<=
(а также
<
,
>
и
>=
) можно сделать доступным с по
мощью группового расширения. На этом этапе методы расширения разме
щаются в объектеодиночке
OrdOps
, как показано в листинге 22.10.
Листинг 22.10. Размещение расширений для Ord в объекте-одиночке
// (Это еще не лучшее решение)
object OrdOps:
extension [T](lhs: T)(using ord: Ord[T])
def < (rhs: T): Boolean = ord.lt(lhs, rhs)
def <= (rhs: T): Boolean = ord.lteq(lhs, rhs)
def > (rhs: T): Boolean = ord.gt(lhs, rhs)
def >= (rhs: T): Boolean = ord.gteq(lhs, rhs)
Имея в своем распоряжении определение
OrdOps из листинга 22.10, пользо
ватели могут добавить в свой код синтаксический сахар с помощью операции импорта, как показано здесь:
def insert[T](x: T, xs: List[T])(using Ord[T]): List[T] =
import OrdOps.*
if xs.isEmpty || x <= xs.head then x :: xs else xs.head :: insert(x, xs.tail)
22 .5 . Методы расширения для гивенов 479
Вместо ord.leqt(x,
xs.head)
можно написать x
<=
xs.head
. К тому же вам на самом деле не нужно имя экземпляра
Ord
, так как вы его больше не ис
пользуете. В итоге
(using ord:
Ord[T])
можно упростить до
(using
Ord[T])
Этот подход работает, но было бы неплохо иметь под рукой этот синтакси
ческий сахар всякий раз, когда доступен экземпляр
Ord
. Поскольку такие ситуации не редкость, Scala ищет givenэкземпляры для применимых рас
ширений. Таким образом, эти расширения лучше всего размещать не в объ
ектеодиночке, таком как
OrdOps
, а в трейте самого класса типов
Ord
. Это позволит гарантировать, что методы расширения можно применять всегда, когда экземпляр класса типов уже находится в области видимости. Это вы
глядело бы так, как показано в листинге 22.11.
Листинг 22.11. Размещение расширения в трейте класса типов trait Ord[T]:
def compare(x: T, y: T): Int def lt(x: T, y: T): Boolean = compare(x, y) < 0
def lteq(x: T, y: T): Boolean = compare(x, y) <= 0
def gt(x: T, y: T): Boolean = compare(x, y) > 0
def gteq(x: T, y: T): Boolean = compare(x, y) >= 0
// (Это лучшее решение)
extension (lhs: T)
def < (rhs: T): Boolean = lt(lhs, rhs)
def <= (rhs: T): Boolean = lteq(lhs, rhs)
def > (rhs: T): Boolean = gt(lhs, rhs)
def >= (rhs: T): Boolean = gteq(lhs, rhs)
Благодаря размещению в трейте самого класса типов методы расширения будут доступны всегда, когда используется givenэкземпляр этого класса.
Например, методы расширения будут просто доступны внутри insert
, и для этого не нужно ничего импортировать. Это видно в листинге 22.12.
Листинг 22.12. Использование расширения, определенного в трейте класса типов def insert[T](x: T, xs: List[T])(using Ord[T]): List[T] =
if xs.isEmpty || x <= xs.head then x :: xs else xs.head :: insert(x, xs.tail)
Поскольку вам больше не нужно импортировать
OrdOps.*
, версия insert
, показанная в листинге 22.12, получилась более компактной по сравнению с предыдущей. Более того, упростилось и само расширение. Сравните реали
зации группового расширения в листингах 22.10 и 22.11. Методы расширения являются частью трейта самого класса типов, поэтому у него уже есть ссылка на экземпляр этого класса, то есть this
. Таким образом, в начале больше не
480 Глава 22 • Методы расширения нужно указывать
[T]
и
(using ord:
Ord[T])
; это выражение упростилось до extension
(lhs:
T)
. К тому же, поскольку у вас больше нет переданного экзем
пляра
Ord[T]
с именем ord
, вы не можете использовать его для вызова методов класса типов, таких как lt и lteq
. Вместо этого их можно вызывать из ссылки this
. Таким образом, ord.lt(lhs,
rhs)
превращается в lt(lhs,
rhs)
Scala переопределяет методы расширения, делая их членами трейта самого класса типов, как показано в листинге 22.13.
Листинг 22.13. Расширения класса типов после переопределения компилятором trait Ord[T]:
def compare(x: T, y: T): Int def lt(x: T, y: T): Boolean = compare(x, y) < 0
def lteq(x: T, y: T): Boolean = compare(x, y) <= 0
def gt(x: T, y: T): Boolean = compare(x, y) > 0
def gteq(x: T, y: T): Boolean = compare(x, y) >= 0
// С внутренними обозначениями расширения:
def < (lhs: T)(rhs: T): Boolean = lt(lhs, rhs)
def <= (lhs: T)(rhs: T): Boolean = lteq(lhs, rhs)
def > (lhs: T)(rhs: T): Boolean = gt(lhs, rhs)
def >= (lhs: T)(rhs: T): Boolean = gteq(lhs, rhs)
Чтобы исправить ошибку выбора типа, Scala заглядывает внутрь givenэкземп
ля ров
Ord[T]
при поиске методов расширений. Для этого компилятор Scala использует немного запутанный алгоритм, который подробно описан далее.
22 .6 . Где Scala ищет методы расширения
Когда компилятор встречает попытку вызвать метод из ссылки на объект, он проверяет, определен ли этот метод в классе самого объекта. Если да, то он выбирает этот метод и не переходит к поиску метода расширения
1
. В про
тивном случае во время компиляции возникает ошибка выбора кандидата.
Но прежде, чем выводить эту ошибку, компилятор ищет метод расширения или неявное преобразование, которые могут ее исправить
2
. Компилятор со
общит об ошибке, только если ему не удастся найти метод расширения или неявное преобразование, которые позволили бы от нее избавиться.
1
Это общее правило: если участок кода компилируется как есть, компилятор Scala не преобразует его во чтото другое.
2
Неявные преобразования будут описаны в главе 23.
Резюме 481
Scala выполняет поиск метода расширения в два этапа. На первом этапе компилятор проверяет лексическую область видимости. На втором он ана
лизирует члены givenэкземпляров в лексической области видимости, члены объектовкомпаньонов класса получателя, родительских классов и трейтов, а также члены givenэкземпляров в этих самых объектахкомпаньонах.
В рамках второго этапа он также пытается выполнить неявное приведение типа получателя.
Если на какомлибо этапе компилятор находит сразу несколько подходящих методов расширения, он выбирает из них самый конкретный, подобно тому как происходит выбор перегруженного метода из нескольких вариантов. Если найдено два и больше метода расширения с одинаковой степенью конкретно
сти, выводится ошибка компиляции со списком равнозначных расширений.
Определение может встречаться в лексической области видимости по одной из трех причин: его определили напрямую, импортировали или унаследова
ли. Например, следующий вызов absOption из
88
успешно компилируется, потому что перед использованием метод расширения absOption импортиру
ется в виде единого идентификатора:
import TwosComplementOps.absOption
88.absOption // Some(88)
Таким образом, поиск методов расширения для absOption заканчивается уже на первом этапе. Для сравнения: поиск, спровоцированный использованием
<=
в листинге 22.12, доходит до второго этапа. Примененным методом рас
ширения выступает
<=
из листинга 22.11. Он вызывается из гивена
Ord[T]
, переданного в виде параметра using
Резюме
Методы расширения позволяют улучшить ваш код за счет синтаксического сахара: все выглядит так, будто функция вызывается из объекта и является методом, объявленным в его классе, хотя на самом деле вы передаете объ
ект этой функции. Из этой главы вы узнали, как определять собственные методы расширения и использовать те, которые определил ктото другой.
Здесь было показано, как методы расширения и классы типов дополняют друг друга и как их лучше всего использовать вместе. В следующей главе мы углубимся в классы типов.
23
Классы типов
Если вам нужно написать функцию, которая реализует поведение, полезное только для какихто определенных типов, в Scala у вас есть несколько вариан
тов. Первый вариант состоит в определении перегруженных методов. Второй — потребовать, чтобы класс любого экземпляра, переданного вашей функции, был примесью в определенном трейте. Третий (и более гибкий) заключается в том, чтобы определить класс типов и адаптировать функцию для работы с типами, для которых определен givenэкземпляр трейта этого класса.
В данной главе мы проведем сравнение этих разных подходов и затем углу
бимся в классы типов. Мы разберемся с синтаксисом классов типов, который привязан к контексту, и рассмотрим несколько примеров таких классов из стандартной библиотеки: для численных литералов, многостороннего равен
ства, неявных преобразований и главных методов. В заключение будет дан пример, иллюстрирующий использование класса типов для сериализации
JSON.
23 .1 . Зачем нужны классы типов
Термин «класс типов» (typeclass) может сбивать с толку в контексте Scala, так как под типами подразумеваются типы языка, а вот класс употребляется в широком смысле и означает группу или множество какихто вещей. Таким образом, «класс типов» — это группа или множество типов.
Как упоминалось в разделе 21.4, классы типов поддерживают специальный
полиморфизм (ad hoc polymorphism), позволяя применять функции с кон
кретным, перечисляемым множеством типов. Любая попытка использования такой функции с типом, который не входит в это перечисляемое множество,
23 .1 . Зачем нужны классы типов
def isMinValue: Boolean = n == Int.MinValue def absOption: Option[Int] =
if !isMinValue then Some(n.abs) else None def negateOption: Option[Int] =
if !isMinValue then Some(-n) else None
В групповом расширении, показанном в листинге 22.5, метод isMinValue вызывается как из absOption
, так и из negateOption
. В таких случаях компи
лятор переопределит вызов так, чтобы он выполнялся из получателя. В дан
ном расширении, к примеру, компилятор подставит n.isMinValue вместо isMinValue
, как показано в листинге 22.6.
Листинг 22.6. Групповое расширение, после переопределения компилятором
// Все с внутренними обозначениями расширения def isMinValue(n: Int): Boolean = n == Int.MinValue def absOption(n: Int): Option[Int] =
if !n.isMinValue then Some(n.abs) else None def negateOption(n: Int): Option[Int] =
if !n.isMinValue then Some(-n) else None
22 .4 . Использование класса типов
Распознавание переполнения в операциях с получением абсолютного зна
чения и изменением знака на противоположный имеет смысл не только для типа
Int
. Любому целочисленному типу, основанному на арифметике в дополнительном коде, свойственна одна и та же проблема с переполне
нием:
22 .4 . Использование класса типов 475
Long.MinValue.abs // -9223372036854775808 (переполнение)
-Long.MinValue // -9223372036854775808 (переполнение)
Short.MinValue.abs // -32768 (переполнение)
-Short.MinValue // -32768 (переполнение)
Byte.MinValue.abs // -128 (переполнение)
-Byte.MinValue // -128 (переполнение)
Если вам нужны безопасные альтернативы abs и unary_- для всех этих ти
пов, вы можете определить отдельное групповое расширение для каждого из них, но в этом случае все реализации будут выглядеть одинаково. Чтобы не дублировать код, вы можете определить вместо этого расширение на основе класса типов. Такое специализированное расширение будет работать для лю
бого типа с givenэкземпляром класса типа.
Чтобы проверить, существует ли трейт с подходящим классом типов, стоит заглянуть в стандартную библиотеку. Трейт
Numeric слишком общий, так как givenэкземпляры предоставляются для типов вроде
Double или
Float
, которые не основаны на арифметике в дополнительном коде. То же самое можно сказать о трейте
Integral
, только вместо
Double или
Float given
экземпляр предоставлен для типа
BigInt
, который не переполняется. Таким образом, самый оптимальный вариант состоит в определении нового трейта специально для целочисленных типов в дополнительном коде, как, например, трейт
TwosComplement
, показанный в листинге 22.7.
После этого следует определить givenэкземпляры для типов в дополни
тельном коде, которые будут содержать методы расширения. Подходящим местом их размещения будет объекткомпаньон, доступ к которому, как вы ожидаете, всегда будет нужен пользователям
1
. В листинге 22.7 given
экземпляры
TwosComplement определены для
Byte
,
Short
,
Int и
Long
Листинг 22.7. Класс типов для чисел в дополнительном коде trait TwosComplement[N]:
def equalsMinValue(n: N): Boolean def absOf(n: N): N
def negationOf(n: N): N
object TwosComplement:
given tcOfByte: TwosComplement[Byte] with def equalsMinValue(n: Byte) = n == Byte.MinValue def absOf(n: Byte) = n.abs def negationOf(n: Byte) = (-n).toByte
1
Совет о том, где лучше определять givenэкземпляры, был дан в разделе 21.5.
476 Глава 22 • Методы расширения given tcOfShort: TwosComplement[Short] with def equalsMinValue(n: Short) = n == Short.MinValue def absOf(n: Short) = n.abs def negationOf(n: Short) = (-n).toShort given tcOfInt: TwosComplement[Int] with def equalsMinValue(n: Int) = n == Int.MinValue def absOf(n: Int) = n.abs def negationOf(n: Int) = -n given tcOfLong: TwosComplement[Long] with def equalsMinValue(n: Long) = n == Long.MinValue def absOf(n: Long) = n.abs def negationOf(n: Long) = -n
Имея в своем распоряжении эти определения, вы можете написать обобщенный метод расширения, как показано в листинге 22.8. Это позволит использовать absOption и negateOption для подходящих типов.
Листинг 22.8. Использование класса типов в расширении
Byte.MaxValue.negateOption // Some(-127)
Byte.MinValue.negateOption // None
Long.MaxValue.negateOption // -9223372036854775807
Long.MinValue.negateOption // None extension [N](n: N)(using tc: TwosComplement[N])
def isMinValue: Boolean = tc.equalsMinValue(n)
def absOption: Option[N] =
if !isMinValue then Some(tc.absOf(n)) else None def negateOption: Option[N] =
if !isMinValue then Some(tc.negationOf(n)) else None
С другой стороны, любая попытка использования этих методов расширения для неподходящих типов приведет к ошибке компиляции:
BigInt(42).negateOption
1 |BigInt(42).negateOption
|ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
|value negateOption is not a member of BigInt.
|An extension method was tried, but could not be
|fully constructed:
|
| negateOption[BigInt](BigInt.apply(42))(
| /* missing */summon[TwosComplement[BigInt]]
| )
Как уже обсуждалось в разделе 21.4, классы типов обеспечивают специаль
ный полиморфизм: функционал, доступный только для определенных типов
(тех, для которых существуют givenэкземпляры класса типов) и выдающий
22 .5 . Методы расширения для гивенов 477
ошибку компиляции для любого другого типа. Классы типов можно исполь
зовать для получения синтаксического сахара в виде методов расширения для определенных типов. Любые попытки применения методов расширения к другим типам не позволят скомпилировать код.
22 .5 . Методы расширения для гивенов
В предыдущем разделе задача класса типов
TwosComplement состояла в пре
доставлении методов расширения для определенного множества типов.
Поскольку основным потребителем методов расширения является поль
зователь, у него не должно вызывать затруднений решение о том, когда их следует делать доступными и стоит ли это делать в принципе. В таких ситуациях расширение лучше всего размещать в объектеодиночке. Ваши пользователи могут импортировать методы расширения из этого объекта в лексическую область видимости, что сделает возможным их применение.
Вы можете, к примеру, поместить групповое расширение для распозна
вания переполнений в объект с именем
TwosComplementOps
, как показано в листинге 22.9.
Листинг 22.9. Размещение методов расширения в объекте-одиночке object TwosComplementOps:
extension [N](n: N)(using tc: TwosComplement[N])
def isMinValue: Boolean = tc.equalsMinValue(n)
def absOption: Option[N] =
if !isMinValue then Some(tc.absOf(n)) else None def negateOption: Option[N] =
if !isMinValue then Some(tc.negationOf(n)) else None
Затем ваши пользователи могут добавить в свой код немного синтаксиче
ского сахара:
import TwosComplementOps.*
Благодаря этому импорту методы расширения будут доступны для при
менения:
-42.absOption // Some(42)
В случае с
TwosComplementOps методы расширения представляют основную цель проектирования, а класс типов играет вспомогательную роль. Но зача
стую все наоборот: класс типов служит главной целью, а методы расширения помогают упростить использование этого класса. В таких ситуациях методы расширения лучше всего размещать в трейте самого класса типов.
478 Глава 22 • Методы расширения
Например, в главе 21 класс типов
Ord был определен, чтобы сделать метод сортировки вставками, isort
, более общим. И хотя эта цель была достигнута с помощью решений, представленных в главе 21 (метод isort можно исполь
зовать с любым типом
T
, для которого доступен givenэкземпляр
Ord[T]
), добавление нескольких методов расширения сделает класс типов
Ord более приятным в использовании.
Каждый трейт с классом типов принимает параметр, поскольку экземпляр этого класса знает, как обращаться с объектами этого типа. Например,
Ord[T]
знает, как сравнивать два экземпляра типа
T
для определения того, какой из них больше или равен другому. Поскольку экземпляр класса типов для
T
— не то же самое, что экземпляр или экземпляры
T
, синтаксис использования классов типов может быть немного громоздким. Например, в листинге 21.1 метод insert принимает givenэкземпляр
Ord[T]
и определяет с его помощью, является ли экземпляр
T
равным начальному элементу уже отсортированного списка или меньше его. Вот как выглядит метод insert из этого листинга:
def insert[T](x: T, xs: List[T])(using ord: Ord[T]): List[T] =
if xs.isEmpty || ord.lteq(x, xs.head) then x :: xs else xs.head :: insert(x, xs.tail)
Вызов ord.lteq(x,
xs.head)
является вполне нормальным, но его можно было бы записать более естественным и, наверное, более понятным образом:
x <= xs.head // Теперь намного понятнее!
Синтаксический сахар
<=
(а также
<
,
>
и
>=
) можно сделать доступным с по
мощью группового расширения. На этом этапе методы расширения разме
щаются в объектеодиночке
OrdOps
, как показано в листинге 22.10.
Листинг 22.10. Размещение расширений для Ord в объекте-одиночке
// (Это еще не лучшее решение)
object OrdOps:
extension [T](lhs: T)(using ord: Ord[T])
def < (rhs: T): Boolean = ord.lt(lhs, rhs)
def <= (rhs: T): Boolean = ord.lteq(lhs, rhs)
def > (rhs: T): Boolean = ord.gt(lhs, rhs)
def >= (rhs: T): Boolean = ord.gteq(lhs, rhs)
Имея в своем распоряжении определение
OrdOps из листинга 22.10, пользо
ватели могут добавить в свой код синтаксический сахар с помощью операции импорта, как показано здесь:
def insert[T](x: T, xs: List[T])(using Ord[T]): List[T] =
import OrdOps.*
if xs.isEmpty || x <= xs.head then x :: xs else xs.head :: insert(x, xs.tail)
22 .5 . Методы расширения для гивенов 479
Вместо ord.leqt(x,
xs.head)
можно написать x
<=
xs.head
. К тому же вам на самом деле не нужно имя экземпляра
Ord
, так как вы его больше не ис
пользуете. В итоге
(using ord:
Ord[T])
можно упростить до
(using
Ord[T])
Этот подход работает, но было бы неплохо иметь под рукой этот синтакси
ческий сахар всякий раз, когда доступен экземпляр
Ord
. Поскольку такие ситуации не редкость, Scala ищет givenэкземпляры для применимых рас
ширений. Таким образом, эти расширения лучше всего размещать не в объ
ектеодиночке, таком как
OrdOps
, а в трейте самого класса типов
Ord
. Это позволит гарантировать, что методы расширения можно применять всегда, когда экземпляр класса типов уже находится в области видимости. Это вы
глядело бы так, как показано в листинге 22.11.
Листинг 22.11. Размещение расширения в трейте класса типов trait Ord[T]:
def compare(x: T, y: T): Int def lt(x: T, y: T): Boolean = compare(x, y) < 0
def lteq(x: T, y: T): Boolean = compare(x, y) <= 0
def gt(x: T, y: T): Boolean = compare(x, y) > 0
def gteq(x: T, y: T): Boolean = compare(x, y) >= 0
// (Это лучшее решение)
extension (lhs: T)
def < (rhs: T): Boolean = lt(lhs, rhs)
def <= (rhs: T): Boolean = lteq(lhs, rhs)
def > (rhs: T): Boolean = gt(lhs, rhs)
def >= (rhs: T): Boolean = gteq(lhs, rhs)
Благодаря размещению в трейте самого класса типов методы расширения будут доступны всегда, когда используется givenэкземпляр этого класса.
Например, методы расширения будут просто доступны внутри insert
, и для этого не нужно ничего импортировать. Это видно в листинге 22.12.
Листинг 22.12. Использование расширения, определенного в трейте класса типов def insert[T](x: T, xs: List[T])(using Ord[T]): List[T] =
if xs.isEmpty || x <= xs.head then x :: xs else xs.head :: insert(x, xs.tail)
Поскольку вам больше не нужно импортировать
OrdOps.*
, версия insert
, показанная в листинге 22.12, получилась более компактной по сравнению с предыдущей. Более того, упростилось и само расширение. Сравните реали
зации группового расширения в листингах 22.10 и 22.11. Методы расширения являются частью трейта самого класса типов, поэтому у него уже есть ссылка на экземпляр этого класса, то есть this
. Таким образом, в начале больше не
480 Глава 22 • Методы расширения нужно указывать
[T]
и
(using ord:
Ord[T])
; это выражение упростилось до extension
(lhs:
T)
. К тому же, поскольку у вас больше нет переданного экзем
пляра
Ord[T]
с именем ord
, вы не можете использовать его для вызова методов класса типов, таких как lt и lteq
. Вместо этого их можно вызывать из ссылки this
. Таким образом, ord.lt(lhs,
rhs)
превращается в lt(lhs,
rhs)
Scala переопределяет методы расширения, делая их членами трейта самого класса типов, как показано в листинге 22.13.
Листинг 22.13. Расширения класса типов после переопределения компилятором trait Ord[T]:
def compare(x: T, y: T): Int def lt(x: T, y: T): Boolean = compare(x, y) < 0
def lteq(x: T, y: T): Boolean = compare(x, y) <= 0
def gt(x: T, y: T): Boolean = compare(x, y) > 0
def gteq(x: T, y: T): Boolean = compare(x, y) >= 0
// С внутренними обозначениями расширения:
def < (lhs: T)(rhs: T): Boolean = lt(lhs, rhs)
def <= (lhs: T)(rhs: T): Boolean = lteq(lhs, rhs)
def > (lhs: T)(rhs: T): Boolean = gt(lhs, rhs)
def >= (lhs: T)(rhs: T): Boolean = gteq(lhs, rhs)
Чтобы исправить ошибку выбора типа, Scala заглядывает внутрь givenэкземп
ля ров
Ord[T]
при поиске методов расширений. Для этого компилятор Scala использует немного запутанный алгоритм, который подробно описан далее.
22 .6 . Где Scala ищет методы расширения
Когда компилятор встречает попытку вызвать метод из ссылки на объект, он проверяет, определен ли этот метод в классе самого объекта. Если да, то он выбирает этот метод и не переходит к поиску метода расширения
1
. В про
тивном случае во время компиляции возникает ошибка выбора кандидата.
Но прежде, чем выводить эту ошибку, компилятор ищет метод расширения или неявное преобразование, которые могут ее исправить
2
. Компилятор со
общит об ошибке, только если ему не удастся найти метод расширения или неявное преобразование, которые позволили бы от нее избавиться.
1
Это общее правило: если участок кода компилируется как есть, компилятор Scala не преобразует его во чтото другое.
2
Неявные преобразования будут описаны в главе 23.
Резюме 481
Scala выполняет поиск метода расширения в два этапа. На первом этапе компилятор проверяет лексическую область видимости. На втором он ана
лизирует члены givenэкземпляров в лексической области видимости, члены объектовкомпаньонов класса получателя, родительских классов и трейтов, а также члены givenэкземпляров в этих самых объектахкомпаньонах.
В рамках второго этапа он также пытается выполнить неявное приведение типа получателя.
Если на какомлибо этапе компилятор находит сразу несколько подходящих методов расширения, он выбирает из них самый конкретный, подобно тому как происходит выбор перегруженного метода из нескольких вариантов. Если найдено два и больше метода расширения с одинаковой степенью конкретно
сти, выводится ошибка компиляции со списком равнозначных расширений.
Определение может встречаться в лексической области видимости по одной из трех причин: его определили напрямую, импортировали или унаследова
ли. Например, следующий вызов absOption из
88
успешно компилируется, потому что перед использованием метод расширения absOption импортиру
ется в виде единого идентификатора:
import TwosComplementOps.absOption
88.absOption // Some(88)
Таким образом, поиск методов расширения для absOption заканчивается уже на первом этапе. Для сравнения: поиск, спровоцированный использованием
<=
в листинге 22.12, доходит до второго этапа. Примененным методом рас
ширения выступает
<=
из листинга 22.11. Он вызывается из гивена
Ord[T]
, переданного в виде параметра using
Резюме
Методы расширения позволяют улучшить ваш код за счет синтаксического сахара: все выглядит так, будто функция вызывается из объекта и является методом, объявленным в его классе, хотя на самом деле вы передаете объ
ект этой функции. Из этой главы вы узнали, как определять собственные методы расширения и использовать те, которые определил ктото другой.
Здесь было показано, как методы расширения и классы типов дополняют друг друга и как их лучше всего использовать вместе. В следующей главе мы углубимся в классы типов.
23
Классы типов
Если вам нужно написать функцию, которая реализует поведение, полезное только для какихто определенных типов, в Scala у вас есть несколько вариан
тов. Первый вариант состоит в определении перегруженных методов. Второй — потребовать, чтобы класс любого экземпляра, переданного вашей функции, был примесью в определенном трейте. Третий (и более гибкий) заключается в том, чтобы определить класс типов и адаптировать функцию для работы с типами, для которых определен givenэкземпляр трейта этого класса.
В данной главе мы проведем сравнение этих разных подходов и затем углу
бимся в классы типов. Мы разберемся с синтаксисом классов типов, который привязан к контексту, и рассмотрим несколько примеров таких классов из стандартной библиотеки: для численных литералов, многостороннего равен
ства, неявных преобразований и главных методов. В заключение будет дан пример, иллюстрирующий использование класса типов для сериализации
JSON.
23 .1 . Зачем нужны классы типов
Термин «класс типов» (typeclass) может сбивать с толку в контексте Scala, так как под типами подразумеваются типы языка, а вот класс употребляется в широком смысле и означает группу или множество какихто вещей. Таким образом, «класс типов» — это группа или множество типов.
Как упоминалось в разделе 21.4, классы типов поддерживают специальный
полиморфизм (ad hoc polymorphism), позволяя применять функции с кон
кретным, перечисляемым множеством типов. Любая попытка использования такой функции с типом, который не входит в это перечисляемое множество,
23 .1 . Зачем нужны классы типов
1 ... 46 47 48 49 50 51 52 53 ... 64