Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 782
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
483
приводит к ошибке компиляции. Например, термин «специальный поли
морфизм» во многих языках изначально описывал то, как операторы вроде
+
или
–
можно сочетать только с определенными типами [Str00]. В Scala для этого используются перегруженные методы. Например, scala.Int содержит семь перегруженных абстрактных методов с именем «минус» (
-
):
def -(x: Double): Double def -(x: Float): Float def -(x: Long): Long def -(x: Int): Int def -(x: Char): Int def -(x: Short): Int def -(x: Byte): Int
Следовательно, методу «минус» из
Int можно передавать экземпляры семи конкретных типов. Это своего рода группа или множество (или класс в об
щем смысле этого слова) типов, принимаемых методом «минус». Это про
иллюстрировано на рис. 23.1.
Рис. 23.1. Множество типов, принимаемых методами «минус» (-) из Int
Еще один способ организации полиморфизма в Scala состоит в использова
нии иерархии классов. Вот пример, в котором для определения семейства цветов используется запечатанный трейт:
sealed trait RainbowColor class Red extends RainbowColor class Orange extends RainbowColor class Yellow extends RainbowColor class Green extends RainbowColor class Blue extends RainbowColor class Indigo extends RainbowColor class Violet extends RainbowColor
На основе этой иерархии можно определить метод, который принимает
Rain- bowCo lor в качестве аргумента:
def paint(rc: RainbowColor): Unit
484 Глава 23 • Классы типов
Поскольку трейт
RainbowColor запечатан, методу paint можно передавать только аргументы одного из восьми типов, показанных на рис. 23.2. С любым другим типом он не скомпилируется. Этот подход можно считать специ
альным полиморфизмом, но его называют подтипизацией (или полимор
физмом подтипов), чтобы подчеркнуть его важную особенность: классы всех экземпляров, передаваемых методу paint
, должны быть примесями в трейте
RainbowColor и соблюдать любые ограничения, установленные его интерфейсом. Для сравнения: типы, принимаемые методом «минус» (
-
) из
Int
(см. рис. 23.1), не обязаны соблюдать правила никакого общего интер
фейса, кроме самого верхнего типа в иерархии Scala,
Any
. Если подытожить, то подтипизация делает возможным полиморфизм родственных типов, тогда как подходы наподобие перегрузки и классов типов позволяют организовать полиморфизм не связанных между собой типов (специальный полиморфизм).
Рис. 23.2. Множество типов, принимаемых методом paint
Ввиду ограничений, накладываемых интерфейсом, подтипизация работает лучше всего, когда иерархии классов определяют небольшие семейства типов, ориентированных на какуюто единую концепцию. Отличными при
мерами являются запечатанные иерархии и перечисления. В таких самодо
статочных семействах типов легко обеспечить совместимость интерфейсов.
Подтипизация также может применяться для моделирования более крупных, незапечатанных семейств, ориентированных на единую концепцию. В каче
стве хорошего примера можно привести библиотеку коллекций Scala. Од
нако при моделировании поведения, широко применяемого к не связанным между собой типам (такого, как сериализация или упорядочение), подход на основе подтипизации становится более громоздким.
Рассмотрим в качестве примера трейт
Ordered из состава Scala, который ис
пользует подтипизацию для моделирования операций упорядочения. Как было показано в разделах 11.2 и 18.7, если сделать трейт
Ordered примесью класса и реализовать абстрактный метод compare
, можно наследовать реали
зации
<
,
>
,
<=
и
>=
. Вы также можете использовать
Ordered в качестве верхней границы для определения метода сортировки, такого как orderedMergeSort из листинга 18.11.
23 .1 . Зачем нужны классы типов 485
Недостаток этого подхода в том, что любой тип
T
, передаваемый методу orde- redMergeSort
, обязан быть примесью типа
Ordered[T]
и соблюдать правила его интерфейса. Из этого следует одна потенциальная проблема: в классе, в который вы примешали
Ordered
, уже могут быть определены методы, чьи имена или контракты конфликтуют с
Ordered
. Еще одна проблема может быть связана с конфликтами вариантности. Представьте, что вы хотите при
мешать
Ordered в перечисление
Hope из раздела 19.4. Возможно, вы надеетесь, что вам удастся реализовать метод compare путем использования объекта
Sad в качестве наименьшего значения
Hope и упорядочения экземпляров
Glad с учетом порядка размещения содержащихся в них объектов. К сожалению, компилятору такой план не понравится, поскольку тип
Hope ковариантный по своему параметру типа, тогда как
Ordered инвариантный:
class Hope[+T <: Ordered[T]] extends Ordered[Hope[T]]
1 |class Hope[+T <: Ordered[T]] extends Ordered[Hope[T]]
|ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
|covariant type T occurs in invariant position in type
|Object with Ordered[Hope[T]] {...} of class Hope
Таким образом одним из потенциальных недостатков полиморфизма подти
пов является наличие несовместимых интерфейсов. Еще одна более распро
страненная проблема связана с существованием совместимых интерфейсов,
которые не поддаются изменению. Например, вы не можете использовать метод orderedMergeSort
, показанный в листинге 18.11, для сортировки
List[Int]
, поскольку
Int не наследует
Ordered[Int]
, — и с этим ничего не поделать. На практике же основной трудностью при использовании под
типизации для общих концепций, применяемых ко многим не связанным между собой типам, является то, что многие из этих типов определены в би
блиотеках, которые нельзя изменить.
Классы типов решают эту проблему за счет определения отдельной иерар
хии, ориентированной на общую концепцию, с использованием параметра для задания типа, возможности которого расширяются. Поскольку эта иерархия ориентирована только на какуюто одну процедуру, такую как сериализация или упорядочение, обеспечение совместимости интерфейсов не составляет труда. Экземпляр класса типов использует параметр для опре
деления типа, который расширяется, поэтому вам не нужно изменять сам тип, чтобы его расширить
1
. Благодаря этому можно с легкостью определять givenэкземпляры класса типов, размещенных в библиотеках, которые вы не можете изменять.
1
Использование параметров типа таким образом называют универсальным поли
морфизмом.
486 Глава 23 • Классы типов
Хорошим примером этого является класс типов
Ordering из состава Scala, ко
торый определяет иерархию, ориентированную на упорядочение. Иерархия типов
Ordering отделена от типов, которые упорядочиваются. В результате, несмотря на то что
Ordered нельзя примешать в
Hope
, вы можете определить для
Hope givenэкземпляр
Ordered
. Это работает, даже несмотря на то, что
Hope находится в библиотеке, недоступной для изменения, и вопреки раз
личиям в вариантности между ковариантным типом
Hope и инвариантным
Ordering
. Реализация показана в листинге 23.1.
Листинг 23.1. Given-экземпляр Ordering для Hope[T]
import org.stairwaybook.enums_and_adts.hope.Hope object HopeUtils:
given hopeOrdering[T](using ord: Ordering[T]): Ordering[Hope[T]] with def compare(lh: Hope[T], rh: Hope[T]): Int =
import Hope.{Glad, Sad}
(lh, rh) match case (Sad, Sad) => 0
case (Sad, _) => 1
case (_, Sad) => +1
case (Glad(lhv), Glad(rhv)) =>
ord.compare(lhv, rhv)
Ordering
— это множество всех типов
T
, для которых существуют given
экземпляры
Ordering[T]
. Стандартная библиотека предоставляет given
экземпляры
Ordering для многих типов, включая
Int и
String
, что делает их стандартными членами класса типов
Ordering
. Гивен hopeOrdering
, показанный в листинге 23.1, добавляет типы класса типов
Ordering вида
Hope[T]
для всех типов
T
, которые также являются членами данного класса.
Множество типов, составляющее класс типов
Ordering
, проиллюстрирова
но на рис. 23.3.
Рис. 23.3. Множество типов T с given-экземплярами Ordering[T]
23 .2 . Границы контекста 487
Классы типов поддерживают специальный полиморфизм, так как вы можете создавать функции, доступные только для типов, для которых существуют givenэкземпляры определенного класса типов. Любая попытка использо
вания таких функций в сочетании с типом, у которого нет givenэкземпляра необходимого класса типов, приведет к ошибке компиляции. Например, методу msort
, показанному в листинге 21.5, можно передать
List[T]
с любым типом
T
, для которого определен givenэкземпляр
Ordering[T]
. Посколь
ку в стандартной библиотеке имеются givenэкземпляры
Ordering[Int]
и
Ordering[String]
, функции msort можно передавать
List[Int]
и
List[String]
. Более того, если импортировать гивен hopeOrdering
, показан
ный в листинге 23.1, msort можно будет также передавать
List[Hope[Int]]
,
List[Hope[String]]
,
List[Hope[Hope[Int]]]
и т. д. С другой стороны, любая попытка передать функции msort список с элементами типа, для которого не был определен экземпляр
Ordering
, приведет к ошибке компиляции.
Если подытожить, то классы типов решают проблему, состоящую в том, что вместить все операции с типом в его иерархию классов слишком сложно, неудобно или попросту невозможно. На практике не всякий тип, с которым нужно выполнить операцию общего характера, может реализовать интер
фейс, который сделает его частью общей иерархии. Подход с использованием классов типов позволяет применять вторую, отдельную иерархию, ориенти
рованную на предоставление операции.
23 .2 . Границы контекста
Классы типов являются важным шаблоном проектирования в Scala, поэто
му для них предусмотрен сокращенный синтаксис под названием «границы
контекста». Возьмем в качестве примера функцию maxList из листинга
23.2, которая возвращает максимальный элемент в переданном списке.
В качестве первого аргумента она принимает
List[T]
, но у нее есть еще один список аргументов, содержащий значение using типа
Ordering[T]
В теле maxList переданный аргумент
Ordering[T]
используется в двух местах: в рекурсивном вызове maxList и в выражении if
, которое прове
ряет, превышает ли начальный элемент значение максимального элемента остальной части списка.
Листинг 23.2. Функция с параметром using def maxList[T](elements: List[T])
(using ordering: Ordering[T]): T =
elements match
488 Глава 23 • Классы типов case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x case x :: rest =>
val maxRest = maxList(rest)(using ordering)
if ordering.gt(x, maxRest) then x else maxRest
Функция maxList демонстрирует использование параметра using для предоставления дополнительной информации о типе, явно указанном в предыдущем списке параметров. В частности, параметр ordering типа
Ordering[T]
дополнительно описывает тип
T
; в данном случае он уточняет, как упорядочивать экземпляры этого типа. Тип
T
фигурирует в
List[T]
, типе параметра elements
, указанном в предыдущем списке параметров.
Поскольку elements всегда нужно указывать явно при вызове maxList
, тип
T
будет известен на этапе компиляции, что позволяет компилятору определить, доступно ли givenопределение
Ordering[T]
. Если да, то компилятор может автоматически передать второй список параметров, ordering
В реализации maxList
, показанной в листинге 23.2, параметр ordering передается явно с помощью using
, однако делать это не обязательно. Ког
да для параметра указано ключевое слово using
, компилятор не только пытается предоставить этому параметру givenзначение, но и определяет его в качестве доступного гивена в теле метода! Таким образом, первое использование ordering в теле метода можно опустить, как показано в листинге 23.3.
Анализируя код из листинга 23.3, компилятор обнаружит несоответствие типов. Выражение maxList(rest)
предоставляет всего один список пара
метров, а maxList требует два. Но, поскольку второй список параметров помечен как using
, компилятор не сразу прекращает проверку типов. Вме
сто этого он ищет givenпараметр подходящего типа, которым является
Ordering[T]
. В этом случае он находит один такой параметр и преобразует вызов в maxList(rest)(using ordering)
, после чего проверка типов проходит успешно.
Листинг 23.3. Функция, внутри которой используется параметр using def maxList[T](elements: List[T])
(using ordering: Ordering[T]): T =
elements match case List() =>
throw new IllegalArgumentException("empty list!")
23 .2 . Границы контекста 489
case List(x) => x case x :: rest =>
val maxRest = maxList(rest) // Использует given-
// экземпляр.
if ordering.gt(x, maxRest) then x // Этот параметр ordering else maxRest // по-прежнему задан явно
Существует также способ опустить второе использование ordering
. Для этого в стандартной библиотеке предусмотрен следующий метод:
def summon[T](using t: T) = t
В результате вызова summon[Foo]
компилятор будет искать givenопределение типа
Foo
. Затем он вызовет метод summon с этим объектом и получит этот же объект в ответ. Таким образом, если вам нужно найти в текущей области ви
димости givenэкземпляр
Foo
, можете просто написать summon[Foo]
. Напри
мер, в листинге 23.4 демонстрируется использование summon[Ordering[T]]
для извлечения параметра ordering по его типу.
Листинг 23.4. Функция, использующая summon def maxList[T](elements: List[T])
(using ordering: Ordering[T]): T =
elements match case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x case x :: rest =>
val maxRest = maxList(rest)
if summon[Ordering[T]].gt(x, maxRest) then x else maxRest
Внимательно взгляните на эту последнюю версию maxList
. В ее теле нет ни единого упоминания параметра ordering
. С тем же успехом второй параметр можно было бы назвать comparator
:
def maxList[T](elements: List[T])
(using comparator: Ordering[T]): T = // то же тело…
Если на то пошло, данная версия тоже работает:
def maxList[T](elements: List[T])
(using iceCream: Ordering[T]): T = ??? // то же тело…
Ввиду распространенности этого приема Scala позволяет опустить имя данного параметра и сократить заголовок метода с помощью границы кон-
текста. Граница контекста позволяет сделать так, чтобы сигнатура maxList
490 Глава 23 • Классы типов выглядела как в листинге 23.5. Границей в данном случае служит синтаксис
[T
:
Ordering]
, и она имеет двойное назначение. Вопервых, она вводит параметр типа
T
, как это обычно происходит. Вовторых, она добавляет параметр using типа
Ordering[T]
. В предыдущей версии maxList этот пара
метр назывался ordering
, но при использовании границы контекста его имя неизвестно. Как было показано ранее, вам зачастую не обязательно знать имя параметра.
Листинг 23.5. Функция с границей контекста def maxList[T : Ordering](elements: List[T]): T =
elements match case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x case x :: rest =>
val maxRest = maxList(rest)
if summon[Ordering[T]].gt(x, maxRest) then x else maxRest
К границе контекста можно относиться как к информации о параметре типа. Когда вы пишете
[T
<:
Ordered[T]]
, это означает, что
T
является ти
пом
Ordered[T]
. Для сравнения
[T
:
Ordering]
говорит не столько о том, что такое
T
, сколько о существовании какойто процедуры упорядочения, относящейся к
T
Границы контекста — это, в сущности, синтаксический сахар для классов типов. Сам факт наличия этого сокращенного синтаксиса в Scala является свидетельством того, насколько полезной идиомой служат классы типов для программирования на этом языке.
23 .3 . Главные методы
Как упоминалось в шаге 2 в главе 2, в Scala главный метод можно объявить с помощью аннотации
@main
. Например:
// В файле echoargs.scala
@main def echo(args: String*) =
println(args.mkString(" "))
Как было проиллюстрировано в главе 2, этот код можно выполнять как скрипт, если запустить scala с именем исходного файла, echoargs.scala
:
$ scala echoargs.scala Running as a script
Running as a script
23 .3 . Главные методы 491
Этот код также можно выполнить в качестве приложения, скомпилировав исходный файл и затем снова вызвав scala
. Но в этом случае нужно указать имя главного метода, echo
:
$ scalac echoargs.scala
$ scala echo Running as an application
Running as an application
Все главные методы, показанные до сих пор, принимают один аргумент с повторяющимися параметрами
String*
, это не является обязательным требованием. В Scala главные методы могут принимать любое количество аргументов любых типов. Вот, например, главный метод, который ожидает получить строку и целое число:
// В файле repeat.scala
@main def repeat(word: String, count: Int) =
val msg =
if count > 0 then val words = List.fill(count)(word)
words.mkString(" ")
else
"Please enter a word and a positive integer count."
println(msg)
Учитывая объявление этого главного метода, при запуске repeat в команд
ной строке необходимо указать одно строковое и одно целочисленное зна
чение:
$ scalac repeat.scala
$ scala repeat hello 3
hello hello hello
Откуда Scala знает, как превратить строковой параметр "3"
в число
3
? Для этого используется класс типов
FromString
, который является членом sca- la.util.CommandLineParser
. Его объявление показано в листинге 23.6.
Листинг 23.6. Трейт класса типов FromString trait FromString[T]:
def fromString(s: String): T
В стандартной библиотеке Scala определены гивены
FromString для несколь
ких часто используемых типов, таких как
String и
Int
. Эти экземпляры на
приводит к ошибке компиляции. Например, термин «специальный поли
морфизм» во многих языках изначально описывал то, как операторы вроде
+
или
–
можно сочетать только с определенными типами [Str00]. В Scala для этого используются перегруженные методы. Например, scala.Int содержит семь перегруженных абстрактных методов с именем «минус» (
-
):
def -(x: Double): Double def -(x: Float): Float def -(x: Long): Long def -(x: Int): Int def -(x: Char): Int def -(x: Short): Int def -(x: Byte): Int
Следовательно, методу «минус» из
Int можно передавать экземпляры семи конкретных типов. Это своего рода группа или множество (или класс в об
щем смысле этого слова) типов, принимаемых методом «минус». Это про
иллюстрировано на рис. 23.1.
Рис. 23.1. Множество типов, принимаемых методами «минус» (-) из Int
Еще один способ организации полиморфизма в Scala состоит в использова
нии иерархии классов. Вот пример, в котором для определения семейства цветов используется запечатанный трейт:
sealed trait RainbowColor class Red extends RainbowColor class Orange extends RainbowColor class Yellow extends RainbowColor class Green extends RainbowColor class Blue extends RainbowColor class Indigo extends RainbowColor class Violet extends RainbowColor
На основе этой иерархии можно определить метод, который принимает
Rain- bowCo lor в качестве аргумента:
def paint(rc: RainbowColor): Unit
484 Глава 23 • Классы типов
Поскольку трейт
RainbowColor запечатан, методу paint можно передавать только аргументы одного из восьми типов, показанных на рис. 23.2. С любым другим типом он не скомпилируется. Этот подход можно считать специ
альным полиморфизмом, но его называют подтипизацией (или полимор
физмом подтипов), чтобы подчеркнуть его важную особенность: классы всех экземпляров, передаваемых методу paint
, должны быть примесями в трейте
RainbowColor и соблюдать любые ограничения, установленные его интерфейсом. Для сравнения: типы, принимаемые методом «минус» (
-
) из
Int
(см. рис. 23.1), не обязаны соблюдать правила никакого общего интер
фейса, кроме самого верхнего типа в иерархии Scala,
Any
. Если подытожить, то подтипизация делает возможным полиморфизм родственных типов, тогда как подходы наподобие перегрузки и классов типов позволяют организовать полиморфизм не связанных между собой типов (специальный полиморфизм).
Рис. 23.2. Множество типов, принимаемых методом paint
Ввиду ограничений, накладываемых интерфейсом, подтипизация работает лучше всего, когда иерархии классов определяют небольшие семейства типов, ориентированных на какуюто единую концепцию. Отличными при
мерами являются запечатанные иерархии и перечисления. В таких самодо
статочных семействах типов легко обеспечить совместимость интерфейсов.
Подтипизация также может применяться для моделирования более крупных, незапечатанных семейств, ориентированных на единую концепцию. В каче
стве хорошего примера можно привести библиотеку коллекций Scala. Од
нако при моделировании поведения, широко применяемого к не связанным между собой типам (такого, как сериализация или упорядочение), подход на основе подтипизации становится более громоздким.
Рассмотрим в качестве примера трейт
Ordered из состава Scala, который ис
пользует подтипизацию для моделирования операций упорядочения. Как было показано в разделах 11.2 и 18.7, если сделать трейт
Ordered примесью класса и реализовать абстрактный метод compare
, можно наследовать реали
зации
<
,
>
,
<=
и
>=
. Вы также можете использовать
Ordered в качестве верхней границы для определения метода сортировки, такого как orderedMergeSort из листинга 18.11.
23 .1 . Зачем нужны классы типов 485
Недостаток этого подхода в том, что любой тип
T
, передаваемый методу orde- redMergeSort
, обязан быть примесью типа
Ordered[T]
и соблюдать правила его интерфейса. Из этого следует одна потенциальная проблема: в классе, в который вы примешали
Ordered
, уже могут быть определены методы, чьи имена или контракты конфликтуют с
Ordered
. Еще одна проблема может быть связана с конфликтами вариантности. Представьте, что вы хотите при
мешать
Ordered в перечисление
Hope из раздела 19.4. Возможно, вы надеетесь, что вам удастся реализовать метод compare путем использования объекта
Sad в качестве наименьшего значения
Hope и упорядочения экземпляров
Glad с учетом порядка размещения содержащихся в них объектов. К сожалению, компилятору такой план не понравится, поскольку тип
Hope ковариантный по своему параметру типа, тогда как
Ordered инвариантный:
class Hope[+T <: Ordered[T]] extends Ordered[Hope[T]]
1 |class Hope[+T <: Ordered[T]] extends Ordered[Hope[T]]
|ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
|covariant type T occurs in invariant position in type
|Object with Ordered[Hope[T]] {...} of class Hope
Таким образом одним из потенциальных недостатков полиморфизма подти
пов является наличие несовместимых интерфейсов. Еще одна более распро
страненная проблема связана с существованием совместимых интерфейсов,
которые не поддаются изменению. Например, вы не можете использовать метод orderedMergeSort
, показанный в листинге 18.11, для сортировки
List[Int]
, поскольку
Int не наследует
Ordered[Int]
, — и с этим ничего не поделать. На практике же основной трудностью при использовании под
типизации для общих концепций, применяемых ко многим не связанным между собой типам, является то, что многие из этих типов определены в би
блиотеках, которые нельзя изменить.
Классы типов решают эту проблему за счет определения отдельной иерар
хии, ориентированной на общую концепцию, с использованием параметра для задания типа, возможности которого расширяются. Поскольку эта иерархия ориентирована только на какуюто одну процедуру, такую как сериализация или упорядочение, обеспечение совместимости интерфейсов не составляет труда. Экземпляр класса типов использует параметр для опре
деления типа, который расширяется, поэтому вам не нужно изменять сам тип, чтобы его расширить
1
. Благодаря этому можно с легкостью определять givenэкземпляры класса типов, размещенных в библиотеках, которые вы не можете изменять.
1
Использование параметров типа таким образом называют универсальным поли
морфизмом.
486 Глава 23 • Классы типов
Хорошим примером этого является класс типов
Ordering из состава Scala, ко
торый определяет иерархию, ориентированную на упорядочение. Иерархия типов
Ordering отделена от типов, которые упорядочиваются. В результате, несмотря на то что
Ordered нельзя примешать в
Hope
, вы можете определить для
Hope givenэкземпляр
Ordered
. Это работает, даже несмотря на то, что
Hope находится в библиотеке, недоступной для изменения, и вопреки раз
личиям в вариантности между ковариантным типом
Hope и инвариантным
Ordering
. Реализация показана в листинге 23.1.
Листинг 23.1. Given-экземпляр Ordering для Hope[T]
import org.stairwaybook.enums_and_adts.hope.Hope object HopeUtils:
given hopeOrdering[T](using ord: Ordering[T]): Ordering[Hope[T]] with def compare(lh: Hope[T], rh: Hope[T]): Int =
import Hope.{Glad, Sad}
(lh, rh) match case (Sad, Sad) => 0
case (Sad, _) => 1
case (_, Sad) => +1
case (Glad(lhv), Glad(rhv)) =>
ord.compare(lhv, rhv)
Ordering
— это множество всех типов
T
, для которых существуют given
экземпляры
Ordering[T]
. Стандартная библиотека предоставляет given
экземпляры
Ordering для многих типов, включая
Int и
String
, что делает их стандартными членами класса типов
Ordering
. Гивен hopeOrdering
, показанный в листинге 23.1, добавляет типы класса типов
Ordering вида
Hope[T]
для всех типов
T
, которые также являются членами данного класса.
Множество типов, составляющее класс типов
Ordering
, проиллюстрирова
но на рис. 23.3.
Рис. 23.3. Множество типов T с given-экземплярами Ordering[T]
23 .2 . Границы контекста 487
Классы типов поддерживают специальный полиморфизм, так как вы можете создавать функции, доступные только для типов, для которых существуют givenэкземпляры определенного класса типов. Любая попытка использо
вания таких функций в сочетании с типом, у которого нет givenэкземпляра необходимого класса типов, приведет к ошибке компиляции. Например, методу msort
, показанному в листинге 21.5, можно передать
List[T]
с любым типом
T
, для которого определен givenэкземпляр
Ordering[T]
. Посколь
ку в стандартной библиотеке имеются givenэкземпляры
Ordering[Int]
и
Ordering[String]
, функции msort можно передавать
List[Int]
и
List[String]
. Более того, если импортировать гивен hopeOrdering
, показан
ный в листинге 23.1, msort можно будет также передавать
List[Hope[Int]]
,
List[Hope[String]]
,
List[Hope[Hope[Int]]]
и т. д. С другой стороны, любая попытка передать функции msort список с элементами типа, для которого не был определен экземпляр
Ordering
, приведет к ошибке компиляции.
Если подытожить, то классы типов решают проблему, состоящую в том, что вместить все операции с типом в его иерархию классов слишком сложно, неудобно или попросту невозможно. На практике не всякий тип, с которым нужно выполнить операцию общего характера, может реализовать интер
фейс, который сделает его частью общей иерархии. Подход с использованием классов типов позволяет применять вторую, отдельную иерархию, ориенти
рованную на предоставление операции.
23 .2 . Границы контекста
Классы типов являются важным шаблоном проектирования в Scala, поэто
му для них предусмотрен сокращенный синтаксис под названием «границы
контекста». Возьмем в качестве примера функцию maxList из листинга
23.2, которая возвращает максимальный элемент в переданном списке.
В качестве первого аргумента она принимает
List[T]
, но у нее есть еще один список аргументов, содержащий значение using типа
Ordering[T]
В теле maxList переданный аргумент
Ordering[T]
используется в двух местах: в рекурсивном вызове maxList и в выражении if
, которое прове
ряет, превышает ли начальный элемент значение максимального элемента остальной части списка.
Листинг 23.2. Функция с параметром using def maxList[T](elements: List[T])
(using ordering: Ordering[T]): T =
elements match
488 Глава 23 • Классы типов case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x case x :: rest =>
val maxRest = maxList(rest)(using ordering)
if ordering.gt(x, maxRest) then x else maxRest
Функция maxList демонстрирует использование параметра using для предоставления дополнительной информации о типе, явно указанном в предыдущем списке параметров. В частности, параметр ordering типа
Ordering[T]
дополнительно описывает тип
T
; в данном случае он уточняет, как упорядочивать экземпляры этого типа. Тип
T
фигурирует в
List[T]
, типе параметра elements
, указанном в предыдущем списке параметров.
Поскольку elements всегда нужно указывать явно при вызове maxList
, тип
T
будет известен на этапе компиляции, что позволяет компилятору определить, доступно ли givenопределение
Ordering[T]
. Если да, то компилятор может автоматически передать второй список параметров, ordering
В реализации maxList
, показанной в листинге 23.2, параметр ordering передается явно с помощью using
, однако делать это не обязательно. Ког
да для параметра указано ключевое слово using
, компилятор не только пытается предоставить этому параметру givenзначение, но и определяет его в качестве доступного гивена в теле метода! Таким образом, первое использование ordering в теле метода можно опустить, как показано в листинге 23.3.
Анализируя код из листинга 23.3, компилятор обнаружит несоответствие типов. Выражение maxList(rest)
предоставляет всего один список пара
метров, а maxList требует два. Но, поскольку второй список параметров помечен как using
, компилятор не сразу прекращает проверку типов. Вме
сто этого он ищет givenпараметр подходящего типа, которым является
Ordering[T]
. В этом случае он находит один такой параметр и преобразует вызов в maxList(rest)(using ordering)
, после чего проверка типов проходит успешно.
Листинг 23.3. Функция, внутри которой используется параметр using def maxList[T](elements: List[T])
(using ordering: Ordering[T]): T =
elements match case List() =>
throw new IllegalArgumentException("empty list!")
23 .2 . Границы контекста 489
case List(x) => x case x :: rest =>
val maxRest = maxList(rest) // Использует given-
// экземпляр.
if ordering.gt(x, maxRest) then x // Этот параметр ordering else maxRest // по-прежнему задан явно
Существует также способ опустить второе использование ordering
. Для этого в стандартной библиотеке предусмотрен следующий метод:
def summon[T](using t: T) = t
В результате вызова summon[Foo]
компилятор будет искать givenопределение типа
Foo
. Затем он вызовет метод summon с этим объектом и получит этот же объект в ответ. Таким образом, если вам нужно найти в текущей области ви
димости givenэкземпляр
Foo
, можете просто написать summon[Foo]
. Напри
мер, в листинге 23.4 демонстрируется использование summon[Ordering[T]]
для извлечения параметра ordering по его типу.
Листинг 23.4. Функция, использующая summon def maxList[T](elements: List[T])
(using ordering: Ordering[T]): T =
elements match case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x case x :: rest =>
val maxRest = maxList(rest)
if summon[Ordering[T]].gt(x, maxRest) then x else maxRest
Внимательно взгляните на эту последнюю версию maxList
. В ее теле нет ни единого упоминания параметра ordering
. С тем же успехом второй параметр можно было бы назвать comparator
:
def maxList[T](elements: List[T])
(using comparator: Ordering[T]): T = // то же тело…
Если на то пошло, данная версия тоже работает:
def maxList[T](elements: List[T])
(using iceCream: Ordering[T]): T = ??? // то же тело…
Ввиду распространенности этого приема Scala позволяет опустить имя данного параметра и сократить заголовок метода с помощью границы кон-
текста. Граница контекста позволяет сделать так, чтобы сигнатура maxList
490 Глава 23 • Классы типов выглядела как в листинге 23.5. Границей в данном случае служит синтаксис
[T
:
Ordering]
, и она имеет двойное назначение. Вопервых, она вводит параметр типа
T
, как это обычно происходит. Вовторых, она добавляет параметр using типа
Ordering[T]
. В предыдущей версии maxList этот пара
метр назывался ordering
, но при использовании границы контекста его имя неизвестно. Как было показано ранее, вам зачастую не обязательно знать имя параметра.
Листинг 23.5. Функция с границей контекста def maxList[T : Ordering](elements: List[T]): T =
elements match case List() =>
throw new IllegalArgumentException("empty list!")
case List(x) => x case x :: rest =>
val maxRest = maxList(rest)
if summon[Ordering[T]].gt(x, maxRest) then x else maxRest
К границе контекста можно относиться как к информации о параметре типа. Когда вы пишете
[T
<:
Ordered[T]]
, это означает, что
T
является ти
пом
Ordered[T]
. Для сравнения
[T
:
Ordering]
говорит не столько о том, что такое
T
, сколько о существовании какойто процедуры упорядочения, относящейся к
T
Границы контекста — это, в сущности, синтаксический сахар для классов типов. Сам факт наличия этого сокращенного синтаксиса в Scala является свидетельством того, насколько полезной идиомой служат классы типов для программирования на этом языке.
23 .3 . Главные методы
Как упоминалось в шаге 2 в главе 2, в Scala главный метод можно объявить с помощью аннотации
@main
. Например:
// В файле echoargs.scala
@main def echo(args: String*) =
println(args.mkString(" "))
Как было проиллюстрировано в главе 2, этот код можно выполнять как скрипт, если запустить scala с именем исходного файла, echoargs.scala
:
$ scala echoargs.scala Running as a script
Running as a script
23 .3 . Главные методы 491
Этот код также можно выполнить в качестве приложения, скомпилировав исходный файл и затем снова вызвав scala
. Но в этом случае нужно указать имя главного метода, echo
:
$ scalac echoargs.scala
$ scala echo Running as an application
Running as an application
Все главные методы, показанные до сих пор, принимают один аргумент с повторяющимися параметрами
String*
, это не является обязательным требованием. В Scala главные методы могут принимать любое количество аргументов любых типов. Вот, например, главный метод, который ожидает получить строку и целое число:
// В файле repeat.scala
@main def repeat(word: String, count: Int) =
val msg =
if count > 0 then val words = List.fill(count)(word)
words.mkString(" ")
else
"Please enter a word and a positive integer count."
println(msg)
Учитывая объявление этого главного метода, при запуске repeat в команд
ной строке необходимо указать одно строковое и одно целочисленное зна
чение:
$ scalac repeat.scala
$ scala repeat hello 3
hello hello hello
Откуда Scala знает, как превратить строковой параметр "3"
в число
3
? Для этого используется класс типов
FromString
, который является членом sca- la.util.CommandLineParser
. Его объявление показано в листинге 23.6.
Листинг 23.6. Трейт класса типов FromString trait FromString[T]:
def fromString(s: String): T
В стандартной библиотеке Scala определены гивены
FromString для несколь
ких часто используемых типов, таких как
String и
Int
. Эти экземпляры на