Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 764
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
404 Глава 18 • Параметризация типов использования вы разрабатываете класс самостоятельно. А вот клиентам данного класса придется вставлять подстановочные символы, и если они сде
лают это неправильно, то применить некоторые важные методы экземпляра станет невозможно. Вариантность — дело непростое, пользователи зачастую понимают ее неправильно и избегают ее, полагая, что подстановочные сим
волы и дженерики для них слишком сложны. При использовании вариант
ности по месту объявления ваши намерения выражаются для компилятора, который выполнит двойную проверку, чтобы убедиться, что метод, который вам нужно сделать доступным, будет действительно доступен.
18 .6 . Контравариантность
До сих пор во всех представленных в данной главе примерах встречалась либо ковариантность, либо нонвариантность. Но бывают такие обстоятель
ства, при которых вполне естественно выглядит и контравариантность.
Рассмотрим, к примеру, трейт каналов вывода, показанный в листинге 18.7.
Листинг 18.7. Контравариантный канал вывода trait OutputChannel[-T]:
def write(x: T): Unit
Здесь трейт
OutputChannel определен с контравариантностью, указан
ной для
T
. Следовательно, получается, что канал вывода для
AnyRef яв
ляется подтипом канала вывода для
String
. Хотя на интуитивном уров
не это может показаться непонятным, в действительности здесь есть определенный смысл. Понять, почему так, можно, рассмотрев возмож
ные действия с
OutputChannel[String]
. Единственная поддерживаемая операция — запись в него значения типа
String
. Аналогичная опера
ция может быть выполнена также в отношении
OutputChannel[AnyRef]
Следовательно, вполне безопасно будет вместо
OutputChannel[String]
подставить
OutputChannel[AnyRef]
. В отличие от этого подставить
Out- putChannel[String]
туда, где требуется
OutputChannel[AnyRef]
, будет небез
опасно. В конце концов, на
OutputChannel[AnyRef]
можно отправить любой объект, а
OutputChannel[String]
требует, чтобы все записываемые значения были строками.
Эти рассуждения указывают на общий принцип разработки систем типов: вполне безопасно предположить, что тип
T
— подтип типа
U
, если значение типа
T
можно подставить там, где требуется значение типа
U
. Это называ
ется принципом подстановки Лисков. Он соблюдается, если
T
поддержи
вает те же операции, что и
U
, и все принадлежащие
T
операции требуют
18 .6 . Контравариантность 405
меньшего, а предоставляют большее, чем соответствующие операции в
U
В случае с каналами вывода
OutputChannel[AnyRef]
может быть подтипом
OutputChannel[String]
, поскольку в обоих типах поддерживается одна и та же операция write и она требует меньшего в
OutputChannel[AnyRef]
, чем в
OutputChannel[String]
. Меньшее означает следующее: от аргумента в первом случае требуется только, чтобы он был типа
AnyRef
, а вот во втором случае от него требуется, чтобы он был типа
String
Иногда в одном и том же типе смешиваются ковариантность и контравари
антность. Известный пример — функциональные трейты Scala. Например, при написании функционального типа
A
=>
B
Scala разворачивает этот код, приводя его к виду
Function1[A,
B]
. Определение
Function1
в стандартной библиотеке использует как ковариантность, так и контравариантность: в листинге 18.8 показано, что трейт
Function1
контравариантен в аргументе функции типа
S
и ковариантен в результирующем типе
T
. Принцип под
становки Лисков здесь не нарушается, поскольку аргументы — это то, что требуется, а вот результаты — то, что предоставляется.
Листинг 18.8. Ковариантность и контравариантность Function1
trait Function1[-S, +T]:
def apply(x: S): T
Рассмотрим в качестве примера приложение, показанное в листинге 18.9.
Здесь класс
Publication содержит одно параметрическое поле title типа
String
. Класс
Book расширяет
Publication и пересылает свой строковый па
раметр title конструктору своего суперкласса. В объектеодиночке
Library определяются набор книг books и метод printBookList
, получающий функ
цию info
, у которой есть тип
Book
=>
AnyRef
. Иными словами, типом един
ственного параметра printBookList является функция, которая получает один аргумент типа
Book и возвращает значение типа
AnyRef
. В приложе
нии
Customer определяется метод getTitle
, получающий в качестве един
ственного своего параметра значение типа
Publication и возвращающий значение типа
String
, которое содержит название переданной публикации
Publication
Листинг 18.9. Демонстрация вариантности параметра типа функции class Publication(val title: String)
class Book(title: String) extends Publication(title)
object Library:
val books: Set[Book] =
Set(
Book("Programming in Scala"),
406 Глава 18 • Параметризация типов
Book("Walden")
)
def printBookList(info: Book => AnyRef) =
for book <- books do println(info(book))
object Customer:
def getTitle(p: Publication): String = p.title def main(args: Array[String]): Unit =
Library.printBookList(getTitle)
Теперь посмотрим на последнюю строку в объекте
Customer
. В ней вызыва
ется принадлежащий
Library метод printBookList
, которому в инкапсули
рованном в значение функции виде передается getTitle
:
Library.printBookList(getTitle)
Эта строка кода проходит проверку на соответствие типу даже притом, что
String
, результирующий тип выполнения функции, является подтипом
AnyRef
, типом результата параметра info метода printBookList
. Данный код проходит компиляцию, поскольку результирующие типы функций объявлены ковариантными (
+T
в листинге 18.8). Если заглянуть в тело printBookList
, то можно получить представление о том, почему в этом есть определенный смысл.
Метод printBookList последовательно перебирает элементы своего списка книг и вызывает переданную ему функцию в отношении каждой книги. Он передает
AnyRef
результат, возвращенный info
, методу println
, который вызывает в отношении этого результата метод toString и выводит на стан
дартное устройство возвращенную им строку. Данный процесс будет рабо
тать со
String
значениями, а также с любыми другими подклассами
AnyRef
, в чем, собственно, и заключается смысл ковариантности результирующих типов функций.
Теперь рассмотрим параметр типа той функции, которая была передана методу printBookList
. Хотя тип параметра, принадлежащего функции info
, объявлен как
Book
, функция getTitle при ее передаче в этот метод получает значение типа
Publication
, а этот тип является для
Book
супертипом. Все это работает, поскольку, хотя типом параметра метода printBookList является
Book
, телу метода printBookList будет разрешено только передать значение типа
Book в функцию. А ввиду того, что параметром типа функции getTitle является
Publication
, телу этой функции будет лишь разрешено обращать
ся к его параметру p
, относящемуся к элементам, объявленным в классе
Publication
. Любой метод, объявленный в классе
Publication
, доступен также в его подклассе
Book
, поэтому все должно работать, в чем, собственно,
18 .7 . Верхние ограничители 407
и заключается смысл контравариантности типов результатов функций. Гра
фическое представление всего вышесказанного можно увидеть на рис. 18.1.
Рис. 18.1. Ковариантность и контравариантность в параметрах типа функции
Код в представленном выше листинге 18.9 проходит компиляцию, посколь
ку
Publication
=>
String является подтипом
Book
=>
AnyRef
, что и показано в центре рис. 18.1. Результирующий тип
Function1
определен в качестве ковариантного, и потому показанное в правой части схемы отношение на
следования двух результирующих типов имеет то же самое направление, что и две функции, показанные в центре. В отличие от этого, поскольку тип параметра функции
Function1
определен в качестве контравариантного, отношение наследования двух типов параметров, показанное в левой части схемы, имеет направление, обратное направлению отношения наследования двух функций.
18 .7 . Верхние ограничители
В листинге 14.2 была показана предназначенная для списков функция сорти
ровки слиянием, получавшая в качестве своего первого аргумента функцию сравнения, а в качестве второго, каррированного, — сортируемый список.
Еще один способ, который может вам пригодиться для организации подоб
ной функции сортировки, заключается в требовании того, чтобы тип списка примешивал трейт
Ordered
. Как упоминалось в разделе 11.2, примешивание
Ordered к классу и реализация в
Ordered одного абстрактного метода, compare
, позволит клиентам сравнивать экземпляры класса с помощью операторов
<
,
>
,
<=
и
>=
. В качестве примера в листинге 18.10 показан трейт
Ordered
, при
мешанный к классу
Person
1 ... 38 39 40 41 42 43 44 45 ... 64
Листинг 18.10. Класс Person, к которому примешан трейт Ordered class Person(val firstName: String, val lastName: String)
extends Ordered[Person]:
def compare(that: Person) =
408 Глава 18 • Параметризация типов val lastNameComparison =
lastName.compareToIgnoreCase(that.lastName)
if lastNameComparison != 0 then lastNameComparison else firstName.compareToIgnoreCase(that.firstName)
override def toString = s"$firstName $lastName"
В результате двух людей можно сравнивать так:
val robert = new Person("Robert", "Jones")
val sally = new Person("Sally", "Smith")
robert < sally // true
Чтобы выставить требование о примешивании
Ordered в тип списка, пере
данного вашей новой функции сортировки, следует задействовать верхний
ограничитель. Он указывается так же, как нижний, за исключением того, что вместо обозначения
>:
, используемого для нижних ограничителей, при
меняется, как показано выше, в листинге 18.11, обозначение
<:
Используя синтаксис
T
<:
Ordered[T]
, вы показываете, что параметр типа
T
имеет верхний ограничитель
Ordered[T]
. Это значит, тип элемента, пере
данного orderedMergeSort
, должен быть подтипом
Ordered
. Следовательно,
List[Person]
можно передать orderedMergeSort
, поскольку
Person приме
шивает
Ordered
Рассмотрим, к примеру, следующий список:
val people = List(
Person("Larry", "Wall"),
Person("Anders", "Hejlsberg"),
Person("Guido", "van Rossum"),
Person("Alan", "Kay"),
Person("Yukihiro", "Matsumoto")
)
Поскольку тип элемента этого списка
Person примешивает
Ordered[Person]
(и поэтому является его подтипом), список можно передать методу orde- redMer geSort
:
scala> val sortedPeople = orderedMergeSort(people)
val sortedPeople: List[Person] = List(Anders Hejlsberg,
Alan Kay, Yukihiro Matsumoto, Guido van Rossum, Larry Wall)
А теперь следует заметить, что, хотя функция сортировки, показанная в том же листинге 18.11, и служит неплохой иллюстрацией верхних ограничителей,
Резюме 409
в действительности это не самый универсальный способ в Scala для разра
ботки функции сортировки, получающей преимущества от трейта
Ordered
Листинг 18.11. Функция сравнения с верхним ограничителем def orderedMergeSort[T <: Ordered[T]](xs: List[T]): List[T] =
def merge(xs: List[T], ys: List[T]): List[T] =
(xs, ys) match case (Nil, _) => ys case (_, Nil) => xs case (x :: xs1, y :: ys1) =>
if x < y then x :: merge(xs1, ys)
else y :: merge(xs, ys1)
val n = xs.length / 2
if n == 0 then xs else val (ys, zs) = xs.splitAt(n)
merge(orderedMergeSort(ys), orderedMergeSort(zs))
Так, функцию orderedMergeSort нельзя использовать для сортировки списка целых чисел, поскольку класс
Int не является подтипом
Ordered[Int]
:
scala> val wontCompile = orderedMergeSort(List(3, 2, 1))
val wontCompile = orderedMergeSort(List(3, 2, 1))
В целях получения более универсального решения в разделе 21.4 будет по
казан порядок использования заданных параметров и типовых классов.
Резюме
В этой главе мы показали ряд техник, применяемых для сокрытия инфор
мации: приватные конструкторы, фабричные методы, абстракцию типов и приватные члены объекта. Кроме этого, продемонстрировали способы указать вариантность типов данных и объяснили, что вариантность означает для реализации класса. И наконец, показали технику, помогающую получить гибкие аннотации вариантности: нижние ограничители для параметров ти
пов методов. В следующей главе мы рассмотрим перечисления.
19
Перечисления
В Scala 3 появилась конструкция enum
, которая позволяет сделать определение иерархий запечатанных case
классов более компактным. Перечисления можно использовать для определения перечисляемых типов данных, распространен
ных в популярных объектноориентированных языках, таких как Java, равно как и в функциональных языках наподобие Haskell, где эти типы относятся к алгебраическим. В Scala эти понятия находятся на противоположных концах спектра, и для их определения используется механизм enum
. В этой главе будут описаны как перечисляемые, так и алгебраические типы данных.
19 .1 . Перечисляемые типы данных
Перечисляемый тип данных (enumerated data type, EDT)
1
полезен в ситуаци
ях, когда вам нужен тип, ограниченный конечным множеством именованных значений. Эти именованные значения называются образцами EDT. Напри
мер, EDT для представления четырех направлений компаса (севера, востока, юга и запада) можно определить так:
enum Direction:
case North, East, South, West
Это простое перечисление сгенерирует запечатанный класс с именем
Direction
2
и объекткомпаньон с четырьмя значениями, объявленными как
1
Несмотря на то что enum чаще встречается в качестве краткого названия перечис
ляемых типов данных, в этой книге мы будем использовать аббревиатуру EDT, поскольку конструкция enum в Scala применяется в том числе и для определения алгебраических типов, которые называют ADT (algebraic data types).
2
Запечатанный класс называется типом перечисления.
19 .1 . Перечисляемые типы данных 411
val
. Значения с именами
North
,
East
,
South и
West будут иметь тип
Direction
С помощью этого определения можно, к примеру, создать метод, который будет инвертировать направление компаса, используя сопоставление с об
разцом, как показано ниже:
import Direction.{North, South, East, West}
def invert(dir: Direction): Direction =
dir match case North => South case East => West case South => North case West => East
Вот несколько примеров использования метода invert
:
invert(North) // Юг invert(East) // Запад
Перечисляемые типы данных называются так, потому что компилятор на
значает каждому образцу порядковый номер типа
Int
. Порядковые номера начинаются с 0 и увеличиваются на единицу для каждого образца в том по
рядке, в котором он объявлен в перечислении. Для доступа к порядковым номерам можно использовать метод ordinal
, который компилятор генери
рует для каждого EDT. Например:
North.ordinal // 0
East.ordinal // 1
South.ordinal // 2
West.ordinal // 3
Компилятор также генерирует метод под названием values в объектеком
паньоне для каждого типа перечисления ETD. Этот метод возвращает
Array со всеми образцами EDT в порядке объявления. Тип элементов массива совпадает с типом перечисления. Например,
Direction.values возвращает
Array[Direction]
с элементами
North
,
East
,
South и
West
(в этом порядке):
Direction.values // Array(North, East, South, West)
Наконец, компилятор добавляет в объекткомпаньон метод valueOf
, который преобразует строку в экземпляр типа перечисления — при условии, что эта строка в точности совпадает с названием одного из образцов. Если соот
ветствий не обнаружено, вы получите сгенерированное исключение. Вот несколько примеров использования этого метода:
Direction.valueOf("North") // Север
Direction.valueOf("East") // Восток
412 Глава 19 • Перечисления
Direction.valueOf("Up")
// IllegalArgumentException: enum case not found: Up
Вы также можете назначать типу EDT параметры. Вот новая версия
Direction
, принимающая значение
Int
, которая представляет угол вывода направления в компасе:
enum Direction(val degrees: Int):
case North extends Direction(0)
case East extends Direction(90)
case South extends Direction(180)
case West extends Direction(270)
Поскольку значение degrees объявлено в виде параметрического поля, оно доступно в любом экземпляре
Direction
. Вот несколько примеров:
import Direction.*
North.degrees // 0
South.degrees // 180
Вы также можете определять свои собственные методы для типа перечисле
ния, размещая их в теле enum
. Например, вы могли бы переопределить метод invert
, показанный ранее, чтобы он стал членом
Direction
:
enum Direction(val degrees: Int):
def invert: Direction =
this match case North => South case East => West case South => North case West => East case North extends Direction(0)
case East extends Direction(90)
case South extends Direction(180)
case West extends Direction(270)
Теперь
Direction сможет себя инвертировать:
North.invert // Юг
East.invert // Запад
Если задать для EDT объекткомпаньон, Scala все так же предоставит методы values и valueOf
, если вы их не определите. Например, вот объекткомпаньон для
Direction с методом, который находит ближайшее направление компаса относительно переданного угла:
19 .1 . Перечисляемые типы данных 413
object Direction:
def nearestTo(degrees: Int): Direction =
val rem = degrees % 360
val angle = if rem < 0 then rem + 360 else rem val (ne, se, sw, nw) = (45, 135, 225, 315)
angle match case a if a > nw || a <= ne => North case a if a > ne && a <= se => East case a if a > se && a <= sw => South case a if a > sw && a <= nw => West
Интеграция с перечислениями Java
Чтобы выявить перечисление Java в Scala, достаточно сделать так, что
бы ваш EDT наследовал java.lang.Enum
, и передать тип перечисления
Scala в качестве параметра типа. Например:
enum Direction extends java.lang.Enum[Direction]:
case North, East, South, West
Помимо стандартных возможностей, которыми обладают EDT в Scala, эта версия
Direction также имеет тип java.lang.Enum
. Например, вы можете воспользоваться методом compareTo
, который определен в java.lang.Enum
:
Direction.East.compareTo(Direction.South) // -1
Объекткомпаньон предлагает как объявленные, так и сгенерированные методы. Вот пример одновременного использования двух методов объекта
Direction
: объявленного nearestTo и сгенерированного values
:
def allButNearest(degrees: Int): List[Direction] =
val nearest = Direction.nearestTo(degrees)
Direction.values.toList.filter(_ != nearest)
Функция allButNearest возвращает список, содержащий все направления, кроме ближайшего относительно переданного угла компаса. Вот пример ее использования:
allButNearest(42) // List(East, South, West)
У перечислений есть одно ограничение: вы не можете определять методы для отдельных образцов. Вместо этого любые методы должны объявляться в качестве членов самого типа перечисления, что сделает их доступными