Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
400 Глава 18 • Параметризация типов Первая из этих двух строк вполне допустима, поскольку StrangeIntQueue — подкласс Queue[Int] , и, если предполагается ковариантность очередей, то Queue[Int] является подтипом Queue[Any] . Вполне допустима и вторая строка, так как String значение можно добавлять в Queue[Any] . Но если взять их вместе, то у этих двух строк проявляется не имеющий никако го смысла эффект применения метода извлечения квадратного корня к строке. Это явно не те простые изменяемые поля, которые делают ковариантные поля ненадежными. Проблема носит более общий характер. Получается, как только обобщенный параметр типа появляется в качестве типа параметра метода, содержащие данный метод класс или трейт в этом параметре типа могут не быть ковариантными. Для очередей метод enqueue нарушает это условие: class Queue[+T]: def enqueue(x: T) = При запуске модифицированного класса очередей, подобного показанному ранее, в компиляторе Scala последний выдаст следующий результат: 17 | def enqueue(x: T) = | ˆˆˆˆ | covariant type T occurs in contravariant position | in type T of value x Переназначаемые поля — частный случай правила, которое не позволяет параметрам типа, имеющим аннотацию + , использоваться в качестве типов параметра метода. Как упоминалось в разделе 16.2, переназначаемое поле var x: T рассматривается в Scala как геттер def x: T и как сеттер def x_=(y: T) Как видите, сеттер имеет параметр поля типа T . Следовательно, этот тип не может быть ковариантным. УСКОРЕННЫЙ РЕЖИМ ЧТЕНИЯ Далее в этом разделе мы рассмотрим механизм, с помощью которого компи- лятор Scala проверяет аннотацию вариантности . Если данные подробности вас пока не интересуют, то можете смело переходить к разделу 18 .5 . Сле- дует усвоить главное: компилятор Scala будет проверять любую аннотацию вариантности, которую вы укажете в отношении параметров типа . Напри- мер, при попытке объявить ковариантный параметр типа (путем добавления знака +), способного вызвать потенциальные ошибки в ходе выполнения программы, программа откомпилирована не будет . 18 .4 . Проверка аннотаций вариантности 401 Чтобы проверить правильность аннотаций вариантности, компилятор Scala классифицирует все позиции в теле класса или трейта как положительные, отрицательные или нейтральные. Под позицией понимается любое место в теле класса или трейта (далее в тексте будет фигурировать только термин «класс»), где может использоваться параметр типа. Например, позицией является каждый параметр значения в методе, поскольку у параметра зна чения метода есть тип. Следовательно, в этой позиции может применяться параметр типа. Компилятор проверяет каждое использование каждого из имеющихся в клас се параметров типа. Параметры типа, для аннотации которых применяется знак + , могут быть задействованы только в положительных позициях, а па раметры, для аннотации которых используется знак - , могут применяться лишь в отрицательных. Параметр типа, не имеющий аннотации вариант ности, может использоваться в любой позиции. Таким образом, он является единственной разновидностью параметров типа, которая применима в ней тральных позициях тела класса. В целях классификации позиций компилятор начинает работу с объявления параметра типа, а затем идет через более глубокие уровни вложенности. По зиции на верхнем уровне объявляемого класса классифицируются как поло жительные. По умолчанию позиции на более глубоких уровнях вложенности классифицируются таким же образом, как и охватывающие их уровни, но есть небольшое количество исключений, в которых классификация меняется. Позиции параметров значений метода классифицируются по перевернутой схеме относительно позиций за пределами метода, там положительная клас сификация становится отрицательной, отрицательная — положительной, а нейтральная классификация так и остается нейтральной. Текущая классификация действует по перевернутой схеме в отношении не только позиций параметров значений методов, но и параметров типа методов. В зависимости от вариантности соответствующего параметра типа классификация иногда бывает перевернута в имеющейся в типе позиции аргумента типа, например, это касается Arg в C[Arg] . Если параметр типа у C аннотирован с помощью знака + , то классификация остается такой же. Если с помощью знака - , то классификация определяется по перевернутой схеме. При отсутствии у параметра типа у C аннотации вариантности текущая клас сификация изменяется на нейтральную. В качестве слегка надуманного примера рассмотрим следующее определе ние класса, в котором несколько позиций аннотированы согласно их клас сификации с помощью обозначений ^+ (для положительных) или ^- (для отрицательных): 402 Глава 18 • Параметризация типов abstract class Cat[-T, +U]: def meow[W—](volume: T—, listener: Cat[U+, T—]—) : Cat[Cat[U+, T—]—, U+]+ Позиции параметра типа W и двух параметров значений, volume и listener , помечены как отрицательные. Если посмотреть на результирующий тип метода meow , то позиция первого аргумента, Cat[U, T] , помечена как отрица тельная, поскольку первый параметр типа у Cat , T , аннотирован с помощью знака – . Тип U внутри этого аргумента опять имеет положительную позицию (после двух перевертываний), а тип T внутри этого аргумента остается в от рицательной. Из всего вышесказанного можно сделать вывод, что отследить позиции вариантностей очень трудно. Поэтому помощь компилятора Scala, проде лывающего эту работу за вас, несомненно, приветствуется. После вычисления классификации компилятор проверяет, используется ли каждый параметр типа только в позициях, которые имеют соответствующую классификацию. В данном случае T используется лишь в отрицательных по зициях, а U — лишь в положительных. Следовательно, класс Cat типизирован корректно. 18 .5 . Нижние ограничители Вернемся к классу Queue . Вы видели, что прежнее определение Queue[T] , по казанное выше в листинге 18.4, не может быть превращено в ковариантное в отношении T , поскольку T фигурирует в качестве типа параметра метода enqueue и находится в отрицательной позиции. К счастью, существует способ выйти из этого положения: можно обобщить enqueue , превратив данный метод в полиморфный (то есть предоставить самому методу enqueue параметр типа), и воспользоваться нижним ограни- чителем его параметра типа. Новая формулировка Queue с реализацией этой идеи показана в листинге 18.6. Листинг 18.6. Параметр типа с нижним ограничителем class Queue[+T] (private val leading: List[T], private val trailing: List[T]): def enqueue[U >: T](x: U) = new Queue[U](leading, x :: trailing) // ... В новом определении enqueue дается параметр типа U , и с помощью синтак сиса U >: T тип T определяется как нижний ограничитель для U . В результате 18 .5 . Нижние ограничители 403 от типа U требуется, чтобы он был супертипом для T 1 . Теперь параметр для enqueue имеет тип U , а не T , а возвращаемое значение метода теперь не Queue[T] , а Queue[U] Предположим, есть класс Fruit , имеющий два подкласса: Apple и Orange С новым определением класса Queue появилась возможность добавить Orange в Queue[Apple] . Результатом будет Queue[Fruit] В этом пересмотренном определении enqueue типы используются правильно. Интуитивно понятно, что если T — более конкретный тип, чем ожидалось (например, Apple вместо Fruit ), то вызов enqueue все равно будет работать, поскольку U ( Fruit ) попрежнему будет супертипом для T ( Apple ) 2 Возможно, новое определение enqueue лучше старого, поскольку имеет более обобщенный характер. В отличие от старой версии новое определение по зволяет добавлять в очередь с элементами типа T элементы произвольного супертипа U . Результат получается типа Queue[U] . Наряду с ковариантностью очереди это позволяет получить правильную разновидность гибкости для моделирования очередей из различных типов элементов вполне естествен ным образом. Это показывает, что в совокупности аннотации вариантности и нижние ограничители — хорошо сыгранная команда. Они являются хорошим при мером разработки, управляемой типами, где типы интерфейса управляют ее детальным проектированием и реализацией. В случае с очередями высока вероятность того, что вы не стали бы продумывать улучшенную реализацию enqueue с нижним ограничителем. Но у вас могло созреть решение сделать очереди ковариантными, в случае чего компилятор указал бы для enqueue на ошибку вариантности. Исправление ошибки вариантности путем добавления нижнего ограничителя придает enqueue большую обобщенность, а очереди в целом делает более полезными. Вдобавок это наблюдение — главная причина того, почему в Scala предпо читаема вариантность по месту объявления (declarationsite variance), а не ва риантность по месту использования (usesite variance), встречающаяся в Java в подстановочных символах (wildcards). В случае вариантности по месту 1 Отношения супертипов и подтипов рефлексивны. Это значит, что тип является одновременно супертипом и подтипом по отношению к себе. Даже притом что T — нижняя граница для U , T все же можно передавать методу enqueue 2 С технической точки зрения произошедшее — переворот для нижних границ. Параметр типа U находится в отрицательной позиции (один переворот), а нижняя граница ( >: T ) — в положительной (два переворота). 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 |