Главная страница
Навигация по странице:

  • Интеграция с перечислениями Java

  • КРАТЧАЙШИЙ ПУТЬ

  • Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста


    Скачать 6.24 Mb.
    НазваниеОдерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
    Дата27.04.2023
    Размер6.24 Mb.
    Формат файлаpdf
    Имя файлаScala. Профессиональное программирование 2022.pdf
    ТипДокументы
    #1094967
    страница43 из 64
    1   ...   39   40   41   42   43   44   45   46   ...   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))
    :5: error: inferred type arguments [Int] do not conform to method orderedMergeSort’s type parameter bounds [T <: Ordered[T]]
    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 до

    1   ...   39   40   41   42   43   44   45   46   ...   64


    написать администратору сайта