Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
418 Глава 19 • Перечисления Byte.MaxValue включительно. Эти восьмибитные целочисленные значе ния являются элементами, составляющими Byte , поэтому мощность этого типа равна 256. Тип Int состоит из 2 32 элементов, что делает его мощность равной 2 32 , или 42 949 672 962. Многие типы, такие как String , имеют неограниченное множество возможных значений и, следовательно, бес конечную мощность. Алгебра применима и к бесконечным мощностям, но для иллюстрации этих понятий я буду использовать типы с относительно небольшими, конечными мощностями. Вы можете объединить несколько типов в новый, составной тип, сделав так, чтобы их мощности следовали закону сложения. Такой составной тип называется типом-суммой. В Scala кратчайшим способом определения типа суммы является перечисление. Например: enum Hope[+T]: case Glad(o: T) case Sad Тип Hope позволяет надеяться на лучшее, но готовиться к худшему. Он похож на тип Option в Scala, где Glad играет роль Some , а Sad — None . Из скольких элементов состоит Hope[T] ? Поскольку это типсумма, мощность Hope[T] равна сумме мощностей составляющих его типов: Glad[T] и Sad Каждый экземпляр Glad[T] служит оберткой для типа T , поэтому мощ ность Glad[T] равна мощности T . Например, мощность Glad[Boolean] – 2, как и Boolean . Составными элементами этого типа являются Glad(true) и Glad(false) Sad — объектодиночка наподобие Unit , поэтому его мощ ность составляет 1. Таким образом, мощность Hope[Boolean] равна 3 ( Glad[Boolean] – 2 и Sad – 1). У этого типа есть три возможных экземпляра: Glad(true) , Glad(false) и Sad . В табл. 19.1 показаны другие примеры. case class Both[A, B](a: A, b: B) Таблица 19.1. Мощность Hope Тип Мощность Составные элементы Hope[Nothing] 0 + 1 = 1 Sad Hope[Unit] 1 + 1 = 2 Glad(()), Sad Hope[Boolean] 2 + 1 = 3 Glad(true), Glad(false), Sad Hope[TrafficLight] 3 + 1 = 4 Glad(Red), Glad(Yellow), Glad(Green), Sad Hope[Byte] 256 + 1 = 257 Glad(Byte.MinValue), … Glad(Byte.MaxValue), Sad 19 .4 . Что делает типы ADT алгебраическими 419 Теперь вы знаете, как происходит сложение. Но что насчет умножения? Аналогичным образом несколько типов можно объединить в один составной так, чтобы их мощности следовали закону умножения. Такой составной тип называют типом-произведением. В Scala кратчайшим способом определения типапроизведения является case класс. Например: тип Both позволяет со четать два значения типов A и B , подобно тому как это делает тип Tuple2 из состава Scala. Из скольких элементов состоит Both[A, B] ? Поскольку это типпроизведение, мощность Both[A, B] равна произведению мощностей со ставляющих его типов, A и B Чтобы перечислить все элементы Both[A, B] , нужно взять сочетание каж дого элемента типа A с каждым элементом типа B . Например, TrafficLight и Boolean имеют мощность 3 и 2 соответственно, поэтому мощность Both[TrafficLight, Boolean] составит 3 × 2, или 6. В табл. 19.2 показано шесть возможных экземпляров этого типа вместе с некоторыми примерами. Таблица 19.2. Мощность Both Тип Мощность Составные элементы Both[Nothing, Nothing] 0 × 0 = 0 Нет элементов Both[Unit, Nothing] 1 × 0 = 0 Нет элементов Both[Unit, Unit] 1 × 1 = 1 Both((), ()) Both[Boolean, Nothing] 2 × 0 = 0 Нет элементов Both[Boolean, Unit] 2 × 1 = 2 Both(false, ()), Both(true, ()) Both[Boolean, Boolean] 2 × 2 = 4 Both(false, false), Both(false, true), Both(true, false), Both(true, true) Both[TrafficLight, Nothing] 3 × 0 = 0 Нет элементов Both[TrafficLight, Unit] 3 × 1 = 3 Both(Red, ()), Both(Yellow, ()), Both(Green, ()) Both[TrafficLight, Boolean] 3 × 2 = 6 Both(Red, false), Both(Red, true), Both(Yellow, false), Both(Yellow, true), Both(Green, false), Both(Green, true) 420 Глава 19 • Перечисления Тип Мощность Составные элементы Both[TrafficLight, TrafficLight] 3 × 3 = 9 Both(Red, Red), Both(Red, Yellow), Both(Red, Green), Both(Yellow, Red), Both(Yellow, Yellow), Both(Yellow, Green), Both(Green, Red), Both(Green, Yellow), Both(Green, Green) В целом, алгебраические типы данных представляют суммы произведений, где образцы формируют варианты типасуммы и каждый образец представляет типпроизведение, который может состоять из любого количества составных типов, начиная с нуля. EDT является частным случаем ADT, в котором каж дый типпроизведение представлен объектомодиночкой. За счет понимания алгебраических свойств своих структур данных вы можете использовать соответствующие математические законы, чтобы до казать наличие определенных характеристик у своего кода. Например, что определенные процедуры рефакторинга сохранят логику вашей программы. Показатели мощности ADT подчиняются законам сложения и вычитания, таким как тождество, коммутативность, ассоциативность и дистрибутив ность. Функциональное программирование нередко предлагает возможность лучше понять свой код в контексте разных разделов математики. Резюме В этой главе вы познакомились с перечислениями в Scala — компактным механизмом определения иерархий запечатанных case классов, формиру ющих перечисляемые и алгебраические типы данных. Вы узнали, что в Scala EDT и ADT находятся на разных концах одного спектра, и рассмотрели суть алгебраических типов. Конструкция enum в Scala делает распространенный подход к функциональному моделированию данных лаконичным и указы вает на то, что EDT и ADT являются важными шаблонами проектирования. Таблица 19.2 (окончание) 20 Абстрактные члены Член класса или трейта называется абстрактным, если у него нет в классе полного определения. Реализовывать абстрактные элементы предполага ется в подклассах того класса, в котором они объявлены. Воплощение этой идеи можно найти во многих объектноориентированных языках. Напри мер, в Java можно объявить абстрактные методы. В Scala тоже, что было показано в разделе 10.2. Но Scala этим не ограничивается, и в нем данная идея реализуется самым универсальным образом: в качестве членов классов и трейтов можно объявлять не только методы, но и абстрактные поля и даже абстрактные типы. В этой главе мы рассмотрим все четыре разновидности абстрактных членов: val и var переменные, методы и типы. Попутно изучим предварительно инициализированные поля, ленивые val переменные и типы, зависящие от пути. 20 .1 . Краткий обзор абстрактных членов В следующем трейте объявляется по одному абстрактному члену каждого вида: абстрактный тип ( T ), метод ( transform ), val переменная ( initial ) и var переменная ( current ): trait Abstract: type T def transform(x: T): T val initial: T var current: T 422 Глава 20 • Абстрактные члены Конкретная реализация трейта Abstract нуждается в заполнении определе ний для каждого из его абстрактных членов. Пример реализации, предостав ляющий эти определения, выглядит так: class Concrete extends Abstract: type T = String def transform(x: String) = x + x val initial = "hi" var current = initial Реализация придает конкретное значение типу T , определяя его в качестве псевдонима типа String . Операция transform конкатенирует предоставлен ную ей строку с нею же самой, а для initial , как и для current , устанавли вается значение "hi" Указанные примеры дают вам первое приблизительное представление о разновидностях абстрактных членов, существующих в Scala. Далее мы рассмотрим подробности, касающиеся этих членов, и объясним, для чего могут пригодиться эти новые формы абстрактных членов, а также члены типы в целом. 20 .2 . Члены-типы В примере, приведенном в предыдущем разделе, было показано, что по нятие «абстракный тип» в Scala означает объявление типа (с ключевым словом type ) в качестве члена класса или трейта, без указания определе ния. Абстрактными могут быть и сами классы, а трейты по определению абстрактные, однако ни один из них не является в Scala тем, что называют абстрактным типом. Абстрактный тип в Scala всегда выступает членом какоголибо класса или трейта, как тип T в трейте Abstract Неабстрактный (или конкретный) члентип, такой как тип T в классе Concrete , можно представить себе в качестве способа определения нового имени, или псевдонима, для типа. К примеру, в классе Concrete типу String дается псевдоним T . В результате везде, где в определении класса Concrete появляется T , подразумевается String . Сюда включаются преобразования типов параметров и результирующих типов, как исходных, так и текущих, в которых при их объявлении в супертрейте Abstract упоминается T . Сле довательно, когда в классе Concrete реализуются эти методы, такие обозна чения T интерпретируются как String Один из поводов использовать члентип — определение краткого описатель ного псевдонима для типа, чье имя длиннее или значение менее понятно, 20 .3 . Абстрактные val-переменные 423 чем у псевдонима. Такие членытипы могут сделать понятнее код класса или трейта. Другое основное применение членовтипов — объявление абстракт ного типа, который должен быть определен в подклассе. Более подробно этот вариант использования, продемонстрированный в предыдущем разделе, мы рассмотрим чуть позже в данной главе. 20 .3 . Абстрактные val-переменные Объявление абстрактной val переменной выглядит следующим образом: val initial: String Val переменной даются имя и тип, но не указывается значение. Оно должно быть предоставлено конкретным определением val переменной в подклассе. Например, в классе Concrete для реализации val переменной используется такой код: val initial = "hi" Объявление в классе абстрактной val переменной применяется, когда в этом классе еще неведомо нужное ей значение, но известно, что переменная в каж дом экземпляре класса получит неизменяемое значение. Объявление абстрактной val переменной напоминает объявление абстракт ного метода без параметров: def initial: String Клиентский код будет ссылаться как на val переменную, так и на метод абсолютно одинаково (то есть obj.initial ). Но если initial является аб страктной val переменной, то клиенту гарантируется, что obj.initial будет при каждом обращении выдавать одно и то же значение. Если initial — абстрактный метод, то данная гарантия соблюдаться не будет, поскольку в таком случае метод initial можно реализовать с помощью конкретного метода, возвращающего при каждом своем вызове разные значения. Иными словами, val переменная ограничивает свою допустимую реализа цию: любая реализация должна быть определением val переменной — она не может быть var или def определением. А вот объявления абстрактных методов можно реализовать как конкретными определениями методов, так и конкретными определениями var переменных. Если взять абстрактный класс Fruit , показанный в листинге 20.1, то класс Apple будет допустимой реализацией подкласса, а класс BadApple — нет. 424 Глава 20 • Абстрактные члены Листинг 20.1. Переопределение абстрактных val-переменных и методов без параметров abstract class Fruit: val v: String // `v' — значение (value) def m: String // `m' — метод (method) abstract class Apple extends Fruit: val v: String val m: String // нормально воспринимаемое переопределение 'def' // в 'val' abstract class BadApple extends Fruit: def v: String // ОШИБКА: переопределять 'val' в 'def' нельзя def m: String 20 .4 . Абстрактные var-переменные Как и для абстрактной val переменной, для абстрактной var переменной объявляются только имя и тип, но не начальное значение. Например, в ли стинге 20.2 показан трейт AbstractTime , в котором объявляются две абстракт ные переменные с именами hour и minute Листинг 20.2. Объявление абстрактных var-переменных trait AbstractTime: var hour: Int var minute: Int Что означают такие абстрактные var переменные, как hour и minute ? В раз деле 16.2 было показано, что var переменные, объявленные в качестве членов класса, оснащаются геттером и сеттером. Это справедливо и для абстрактных var переменных. Если, к примеру, объявляется абстрактная var переменная по имени hour , то подразумевается, что для нее объявляется абстрактный геттер hour и абстрактный сеттер hour_= . Тем самым не определяется никакое переназначаемое поле, а конкретная реализация абстрактной var переменной будет выполнена в подклассах. Например, определение AbstractTime , по казанное выше, в листинге 20.2, абсолютно эквивалентно определению, показанному в листинге 20.3. Листинг 20.3. Расширение абстрактных var-переменных в геттеры и сеттеры trait AbstractTime: def hour: Int // get-метод для 'hour' def hour_=(x: Int): Unit // set-метод для 'hour' def minute: Int // get-метод для 'minute' def minute_=(x: Int) : Unit // set-метод для 'minute' 20 .5 . Инициализация абстрактных val-переменных 425 20 .5 . Инициализация абстрактных val-переменных Иногда абстрактные val переменные играют роль, аналогичную роли пара метров суперкласса: они позволяют предоставить в подклассе подробности, не указанные в суперклассе. Рассмотрим в качестве примера переформу лировку класса Rational из главы 6, который был показан в листинге 6.5, в трейт: trait RationalTrait: val numerArg: Int val denomArg: Int У класса Rational из главы 6 были два параметра: n для числителя рацио нального числа и d для его знаменателя. Представленный здесь трейт Ra- tio nalTrait определяет вместо них две абстрактные val переменные: numerArg и denomArg . Чтобы создать конкретный экземпляр этого трейта, нужно реализовать определения абстрактных val переменных, например: new RationalTrait: val numerArg = 1 val denomArg = 2 Здесь появляется ключевое слово new перед RationalTrait , после которого стоит двоеточие и отступ от тела класса. Это выражение выдает экземпляр анонимного класса, примешивающего трейт и определяемого телом. Создание экземпляра данного анонимного класса дает эффект, аналогичный созданию экземпляра с помощью кода new Rational(1, 2) Но аналогия здесь неполная. Есть небольшое различие, касающееся порядка, в котором инициализируются выражения. При записи следующего кода: new Rational(expr1, expr2) два выражения, expr1 и expr2 , вычисляются перед инициализацией класса Rational , следовательно, значения expr1 и expr2 доступны для инициализа ции класса Rational С трейтами складывается обратная ситуация. При записи кода new RationalTrait: val numerArg = expr1 val denomArg = expr2 выражения expr1 и expr2 вычисляются как часть инициализации аноним ного класса, но анонимный класс инициализируется после RationalTrait 426 Глава 20 • Абстрактные члены Следовательно, значения numerArg и denomArg в ходе инициализации RationalTrait недоступны (точнее говоря, выбор любого значения даст значение по умолчанию для типа Int , то есть ноль). Для представленного ранее определения RationalTrait это не проблема, поскольку при инициа лизации трейта значения numerArg или denomArg не используются. Но про блема возникает в варианте RationalTrait , показанном в листинге 20.4, где определяются нормализованные числитель и знаменатель. Листинг 20.4. Трейт, использующий абстрактные val-переменные trait RationalTrait: val numerArg: Int val denomArg: Int require(denomArg != 0) private val g = gcd(numerArg, denomArg) val numer = numerArg / g val denom = denomArg / g private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) override def toString = s"$numer/$denom" При попытке создать экземпляр этого трейта с какимилибо выражениями для числителя и знаменателя, не являющимися простыми литералами, вы дается исключение: scala> val x = 2 val x: Int = 2 scala> new RationalTrait: val numerArg = 1 * x val denomArg = 2 * x java.lang.IllegalArgumentException: requirement failed at scala.Predef$.require(Predef.scala:280) at RationalTrait.$init$( ... 28 elided Исключение в этом примере было сгенерировано потому, что при инициа лизации класса RationalTrait у denomArg сохранилось исходное нулевое значение, изза чего вызов require завершился сбоем. В данном примере показано, что порядок инициализации для параметров класса и абстрактных полей разный. Аргумент параметра класса вычисляется до его передачи конструктору класса (если только это не параметр, передава емый по имени). А вот реализация определения val переменной, которая на ходится в подклассе, вычисляется только после инициализации суперкласса. Теперь вы понимаете, почему поведение абстрактных val переменных отличается от поведения параметров, и было бы неплохо узнать, что 20 .5 . Инициализация абстрактных val-переменных 427 с этим делать. Получится ли определить RationalTrait , который можно надежно инициализировать, не опасаясь, что возникнут ошибки изза не инициализированных полей? В действительности в Scala предлагаются два альтернативных решения этой проблемы: параметрические поля трейтов и ленивые val -переменные. Эти решения рассматриваются в остальной части раздела. Параметрические поля трейтов Первое решение — параметрическиеполя трейтов — позволяет вычислять значения для полей трейтов до того, как сам трейт будет инициализирован. Для этого определите поля как параметрические. Пример приведен в ли стингах 20.5 и 20.6. |