Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
Листинг 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) У перечислений есть одно ограничение: вы не можете определять методы для отдельных образцов. Вместо этого любые методы должны объявляться в качестве членов самого типа перечисления, что сделает их доступными 414 Глава 19 • Перечисления во всех его образцах 1 . Образцы перечисления в первую очередь нужны для предоставления фиксированного множества способов создания экземпляров типа перечисления. 19 .2 . Алгебраические типы данных Алгебраический тип данных (algebraic data type, ADT) состоит из конечного набора образцов. Это естественный способ выражения моделей предметной области, позволяющий моделировать данные для каждого отдельного об разца, который представляет один «конструктор данных» — определенный механизм создания экземпляра типа. В Scala запечатанное семейство case классов составляет ADT — при условии, что как минимум один образец при нимает параметры 2 . Например, вот тип ADT, описывающий три возможно сти: ожидаемое значение («хороший» тип), ошибочное значение («плохой» тип) и исключение («злой» тип): enum Eastwood[+G, +B]: case Good(g: G) case Bad(b: B) case Ugly(ex: Throwable) Как и в случае с EDT, вы не можете определять методы ни для каких кон кретных образцов, будь то Good , Bad или Ugly , но это можно сделать из общего суперкласса Eastwood . Вот пример метода map , который преобразует значение Good , если Eastwood является Good : enum Eastwood[+G, +B]: def map[G2](f: G => G2): Eastwood[G2, B] = this match case Good(g) => Good(f(g)) case Bad(b) => Bad(b) case Ugly(ex) => Ugly(ex) case Good(g: G) case Bad(b: B) case Ugly(ex: Throwable) А вот пример его использования: 1 Вы могли бы определять методы расширения для типов образцов, но в таких ситуа циях, наверное, лучше вручную написать иерархию запечатанных классовобразцов. 2 Для сравнения, EDT — это запечатанное семейство классовобразцов, ни один об разец которого не принимает параметры. 19 .2 . Алгебраические типы данных 415 val eastWood = Good(41) eastWood.map(n => n + 1) // Good(42) Реализация ADT и EDT немного отличается. Для каждого образца ADT, принимающего параметры, компилятор генерирует case класс в объекте компаньоне типа перечисления. Таким образом, для Eastwood компилятор сгенерирует код, похожий на следующий: // Сгенерированный запечатанный трейт ("тип перечисления") sealed trait Eastwood[+G, +B] object Eastwood: // Generated companion object // Сгенерированные классы-образцы case class Good[+G, +B](g: G) extends Eastwood[G, B] case class Bad[+G, +B](b: B) extends Eastwood[G, B] case class Ugly[+G, +B](ex: Throwable) extends Eastwood[G, B] Несмотря на то что итоговый тип фабричного метода, созданного case классами, будет соответствовать типам отдельных классовэкземпляров, компилятор расширит последние до более общего типа перечисления. Вот несколько примеров: scala> Good(42) val res0: Eastwood[Int, Nothing] = Good(42) scala> Bad("oops") val res1: Eastwood[Nothing, String] = Bad(oops) scala> Ugly(new Exception) val res2: Eastwood[Nothing, Nothing] = Ugly(java.lang.Exception) Если вам нужен более конкретный тип для своего образца, можете создать экземпляр с помощью new вместо фабричного метода. Например, Good(1) будет иметь тип Eastwood[Int, Nothing] , однако у new Good(1) будет более конкретный тип, Good[Int, Nothing] ADT могут быть рекурсивными. Например, образец может принимать тип перечисления в качестве параметра. Хорошим примером такого рекурсивного ADT является связный список. Его можно определить в виде запечатанного типа с двумя подтипами: объектомодиночкой, представляющим пустой спи сок, и классом :: , который принимает два параметра — элемент (начало, или head ) и остальную часть списка (конец, или tail ). Ниже показан тип связного списка, в котором объект с пустым списком называется Nada , а класс :: — Yada : enum Seinfeld[+E]: def ::[E2 >: E](o: E2): Seinfeld[E2] = Yada(o, this) case Yada(head: E, tail: Seinfeld[E]) case Nada 416 Глава 19 • Перечисления ADT Seinfeld является рекурсивным типом, поскольку образец Yada при нимает другой тип Seinfeld[E] в качестве своего параметра tail . Учитывая, что Seinfeld объявляет метод :: , вы можете создать экземпляр, который похож на List из состава Scala, но начинается с Nada , а не с Nil : scala> val xs = 1 :: 2 :: 3 :: Nada val xs: Seinfeld[Int] = Yada(1,Yada(2,Yada(3,Nada))) 19 .3 . Обобщенные ADT Обобщенные алгебраические типы данных (generalized algebraic data types, GADT) — это ADT, в которых запечатанный трейт принимает параметр типа, который заполняется образцами. Например: enum Literal[T]: case IntLit(value: Int) extends Literal[Int] case LongLit(value: Long) extends Literal[Long] case CharLit(value: Char) extends Literal[Char] case FloatLit(value: Float) extends Literal[Float] case DoubleLit(value: Double) extends Literal[Double] case BooleanLit(value: Boolean) extends Literal[Boolean] case StringLit(value: String) extends Literal[String] Перечисление Literal представляет GADT, поскольку оно принимает па раметр типа T , который указывается каждым его образцом в инструкции extends . Например, образец IntLit уточняет T до Int , расширяя Literal[Int] Такого рода иерархия запечатанных типов носит специальное название «обобщенные ADT», ввиду особых проблем, которые она создает для про верки и вывода типов. Вот наглядный пример: import Literal.* def valueOfLiteral[T](lit: Literal[T]): T = lit match case IntLit(n) => n case LongLit(m) => m case CharLit(c) => c case FloatLit(f) => f case DoubleLit(d) => d case BooleanLit(b) => b case StringLit(s) => s Метод valueOfLiteral передает средство проверки типов, хотя ни один из его вариантов сопоставления не приводит к нужному итоговому типу T . На пример, вариант case IntLit(n) выдает значение n , которое имеет тип Int 19 .4 . Что делает типы ADT алгебраическими 417 Проблема в том, что Int не является ни типом T , ни его подтипом. Проверка этого типа происходит только лишь изза того, что, как замечает компилятор, для образца IntList роль T может играть только Int . То же самое касается других вариантов. Кроме того, этот более конкретный тип передается об ратно вызывающей стороне. Вот несколько примеров: valueOfLiteral(BooleanLit(true)) // true: Boolean valueOfLiteral(IntLit(42)) // 42: Int 19 .4 . Что делает типы ADT алгебраическими ADT называют алгебраическими, потому что они представляют собой при менение алгебраической теории к типам. Эту связь с математикой можно наблюдать при сопоставлении каждого типа с его мощностью — количеством элементов, из которых он состоит. Если представить тип в виде множества значений, то его мощность будет равна мощности (количеству элементов) этого множества. КРАТЧАЙШИЙ ПУТЬ Этот раздел содержит материал о математических свойствах типов данных, которые можно определить с помощью enum . Если вы хотите вместо этого познакомиться с коллекциями Scala, можете переходить к следующей главе . Например, у Boolean есть два возможных значения, true и false . Это два элемента, из которых состоит тип Boolean . Таким образом, мощность Boolean составляет 2. У типа Unit есть всего одно возможное значение — пустое мно жество () , — поэтому его мощность — 1, а у типа Nothing — 0, так как он не содержит никаких элементов. Вы можете найти или определить другие типы с мощностью 0, 1 или 2, од нако Nothing , Unit и Boolean будет достаточно, чтобы проиллюстрировать алгебраические свойства. Что насчет типа мощностью 3? Если вам не при ходит на ум очевидных вариантов из стандартной библиотеки, вы можете легко создать такой тип с помощью EDT: enum TrafficLight: case Red, Yellow, Green У типа TrafficLight есть три возможных значения: Red , Yellow и Green , что делает его мощность равной 3. Некоторые типы имеют очень большую мощность. Например, у типа Byte есть 256 (2 8 ) возможных значений в диапазоне от Byte.MinValue до |