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

  • Таблица 19.1.

  • Таблица 19.2.

  • Таблица 19.2

  • Листинг 20.2.

  • Листинг 20.3.

  • Листинг 20.4.

  • 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
    страница44 из 64
    1   ...   40   41   42   43   44   45   46   47   ...   64
    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$(:4)
    ... 28 elided
    Исключение в этом примере было сгенерировано потому, что при инициа­
    лизации класса
    RationalTrait у denomArg сохранилось исходное нулевое значение, из­за чего вызов require завершился сбоем.
    В данном примере показано, что порядок инициализации для параметров класса и абстрактных полей разный. Аргумент параметра класса вычисляется
    до его передачи конструктору класса (если только это не параметр, передава­
    емый по имени). А вот реализация определения val
    ­переменной, которая на­
    ходится в подклассе, вычисляется только после инициализации суперкласса.
    Теперь вы понимаете, почему поведение абстрактных val
    ­переменных отличается от поведения параметров, и было бы неплохо узнать, что

    20 .5 . Инициализация абстрактных val-переменных 427
    с этим делать. Получится ли определить
    RationalTrait
    , который можно надежно инициализировать, не опасаясь, что возникнут ошибки из­за не­
    инициализированных полей? В действительности в Scala предлагаются два альтернативных решения этой проблемы: параметрические поля трейтов и ленивые
    val
    -переменные. Эти решения рассматриваются в остальной части раздела.
    Параметрические поля трейтов
    Первое решение — параметрическиеполя трейтов — позволяет вычислять значения для полей трейтов до того, как сам трейт будет инициализирован.
    Для этого определите поля как параметрические. Пример приведен в ли­
    стингах 20.5 и 20.6.
    1   ...   40   41   42   43   44   45   46   47   ...   64


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