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

  • Листинг 13.3.

  • 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
    страница29 из 64
    1   ...   25   26   27   28   29   30   31   32   ...   64
    Листинг 12.14. Объект пакета
    // в файле ShowFruit.scala package bobsdelights def showFruit(fruit: Fruit) =
    import fruit.*
    s"${name}s are $color"
    // в файле PrintMenu.scala package printmenu

    262 Глава 12 • Пакеты, импорты и экспорты import bobsdelights.Fruits import bobsdelights.showFruit object PrintMenu:
    def main(args: Array[String]) =
    println(
    for fruit <- Fruits.menu yield showFruit(fruit)
    )
    При наличии этого определения любой другой код в любом пакете может импортировать метод точно так же, как класс. Например, в данном листинге показан самостоятельный объект
    PrintMenu
    , который находится в другом пакете.
    PrintMenu может импортировать вспомогательный метод showFruit так же, как и класс
    Fruit
    Забегая вперед, следует отметить, что есть и другие способы использова­
    ния определений верхнего уровня, которые еще не были вам показаны.
    Эти определения часто применяются для содержания псевдонимов типов, предназначенных для всего пакета (см. главу 20) и методов расширения
    (см. главу 22). Пакет scala включает определения верхнего уровня, которые доступны всему коду Scala.
    12 .7 . Экспорты
    В разделе 10.11 мы рекомендовали предпочитать композицию, а не наследо­
    вание, особенно если вашей основной целью является повторное использо­
    вание кода. Это применение принципа наименьшей мощности: композиция рассматривает компоненты как «черные ящики», а наследование путем переопределения влияет на их внутреннюю работу. Иногда тесная связь, подразумеваемая наследованием, является лучшим решением проблемы, но там, где в этом нет необходимости, лучше использовать более слабую связь композиции.
    В большинстве популярных объектно­ориентированных языков програм­
    мирования проще использовать наследование. Например, в Scala 2 для него требовалось только предложение extends
    , в то время как композиция требо­
    вала подробного описания последовательности серверов пересылки. Таким образом, подавляющая часть объектно­ориентированных языков подтал­
    кивала программистов к решению, которое зачастую оказывается слишком мощным. Экспорты в Scala 3 направлены на устранение этого дисбаланса.
    Эта новая функция помогает выразить отношения композиции так же крат­

    12 .7 . Экспорты 263
    ко и просто, как и отношения наследования. Она обеспечивает большую гибкость, чем директива extends
    , поскольку в ней можно переименовывать или исключать элементы.
    В качестве примера представьте, что вы хотите создать тип для представле­
    ния целых положительных чисел. Вы могли бы определить его следующим образом
    1
    :
    case class PosInt(value: Int):
    require(value > 0)
    Этот класс позволяет указать в типе, что целое число является положитель­
    ным. Однако для выполнения любых арифметических операций с
    Int в этом коде вам потребуется получить доступ к значению value
    :
    val x = PosInt(88)
    x.value + 1 // 89
    Вы можете сделать это удобнее, реализовав метод
    +
    на
    PosInt
    , который де­
    легируется методу
    +
    базового значения value
    , например, так:
    case class PosInt(value: Int):
    require(value > 0)
    def +(x: Int): Int = value + x
    С добавлением этого метода пересылки теперь можно выполнять сложение целых чисел в
    PosInts без необходимости доступа к значению value
    :
    val x = PosInt(77)
    x + 1 // 78
    Вы могли бы сделать
    PosInt еще более удобным, реализовав все методы
    Int
    , но их более ста. Если бы вы могли определить
    PosInt как подкласс
    Int
    , вы бы унаследовали все эти методы и вам бы не понадобилась их повторная реализация. Но поскольку
    Int является окончательным, вы не можете этого сделать. Вот почему класс
    PosInt должен использовать композицию и деле­
    гирование вместо наследования.
    В Scala 3 вы можете использовать ключевое слово export для указания нуж­
    ных вам методов пересылки, и компилятор сгенерирует их для вас. Вот как можно создать класс
    PosInt
    , который объявляет методы пересылки к соот­
    ветствующим именованным методам базового значения value
    :
    1
    Два альтернативных способа создания этого типа, которые позволяют избежать упаковки, — это
    AnyVals и непрозрачные типы.
    AnyVals будут рассмотрены в раз­
    деле 17.4.

    264 Глава 12 • Пакеты, импорты и экспорты case class PosInt(value: Int):
    require(value > 0)
    export value.*
    С помощью этой конструкции вы можете вызывать любые методы в
    PosInt
    , которые объявлены непосредственно на
    Int
    :
    val x = PosInt(99)
    x + 1 // 100
    x — 1 // 98
    x / 3 // 33
    Директива экспорта создает окончательные методы, называемые экспорт-
    ными псевдонимами, для каждой перегруженной формы каждого имени экс­
    портируемого метода. Например, метод
    +
    , который принимет значение
    Int
    , будет иметь такую подпись в
    PosInt
    :
    final def +(x: Int): Int = value + x
    Вам доступны все различные формы синтаксиса для импорта с экспортом.
    Например, вы можете не использовать операторы символьного сдвига
    <<
    ,
    >>
    и
    >>>
    в
    PosInt
    :
    val x = PosInt(24)
    x << 1 // 48 (shift left)
    x >> 1 // 12 (shift right)
    x >>> 1 // 12 (unsigned shift right)
    У вас есть возможность переименовывать эти операторы при экспорте так же, как и идентификаторы при импорте, — с помощью as
    . Давайте рассмо­
    трим пример:
    case class PosInt(value: Int):
    require(value > 0)
    export value.{<< as shl, >> as shr, >>> as ushr, *}
    С учетом этой директивы экспорта операторы сдвига в
    PosInt больше не будут иметь символьных имен:
    val x = PosInt(24)
    x shl 1 // 48
    x shr 1 // 12
    x ushr 1 // 12
    Вы также можете исключить методы из экспорта подстановочных знаков с помощью as
    _
    . Например, сдвиг вправо (
    >>
    ) и беззнаковый сдвиг вправо
    (
    >>>
    ) всегда дают одинаковый результат для целого положительного числа,

    Резюме 265
    поэтому возможно использование только одного оператора сдвига впра­
    во — shr
    . Этого можно добиться, опустив оператор
    >>>
    с помощью
    >>>
    as
    _
    , например, так:
    case class PosInt(value: Int):
    require(value > 0)
    export value.{<< as shl, >> as shr, >>> as _, *}
    Теперь для метода
    >>>
    не создается никакого псевдонима:
    scala> val x = PosInt(39)
    val x: PosInt = PosInt(39)
    scala> x shr 1
    val res0: Int = 19
    scala> x >>> 1 1 |x >>> 1
    |ˆˆˆˆˆ
    |value >>> is not a member of PosInt
    Резюме
    В данной главе мы показали основные конструкции, предназначенные для разбиения программы на пакеты. Благодаря этому вы имеете простую и по­
    лезную разновидность модульности и можете работать с весьма большими объемами кода, не допуская взаимного влияния различных его частей.
    Существующая в Scala система по духу аналогична пакетированию, ис­
    пользуемому в Java, но с некоторыми отличиями: в Scala проявляется более последовательный или же более универсальный подход. Вы также видели новую функцию, exports
    , которая призвана сделать композицию такой же удобной, как и наследование, для повторного использования кода.
    В следующей главе мы переключимся на сопоставление шаблонов.

    13
    Сопоставление с образцом
    Данная глава описывает понятия case-классов и сопоставления с образцом
    (pattern matching) — конструкций, способствующих созданию обычных, неинкапсулированных структур данных. Особенно полезны эти две кон­
    струкции при работе с древовидными рекурсивными данными.
    Если вам уже приходилось программировать на функциональном языке, то, возможно, сопоставление с образцом вам уже знакомо. А вот понятие case
    ­классов будет для вас новым. case
    ­классы в Scala позволяют применять сопоставление с образцом к объектам, добавляя к ним лишь ключевое слово case
    , не требуя при этом большого объема шаблонного кода.
    Эту главу мы начнем с примера case
    ­классов и сопоставления с образцом.
    Затем разберем все виды поддерживаемых шаблонов, рассмотрим роль за-
    печатанных классов (sealed classes), обсудим перечисления,
    Options и пока­
    жем некоторые неочевидные места в языке, где используется сопоставление с образцом, а также более объемный и приближенный к реальному пример его использования.
    13 .1 . Простой пример
    Прежде чем вникать во все правила и нюансы сопоставления с образцом, есть смысл рассмотреть простой пример, дающий общее представление. До­
    пустим, нужно написать библиотеку, которая работает с арифметическими выражениями и, возможно, является частью разрабатываемого предметно­
    ориентированного языка.
    Первым шагом к решению этой задачи будет определение входных данных.
    Чтобы ничего не усложнять, сконцентрируемся на арифметических выра­

    13 .1 . Простой пример 267
    жениях, состоящих из переменных, чисел и унарных и бинарных операций.
    Все это выражается иерархией классов Scala, показанной в листинге 13.1.
    Листинг 13.1. Определение case-классов trait Expr case class Var(name: String) extends Expr case class Num(number: Double) extends Expr case class UnOp(operator: String, arg: Expr) extends Expr case class BinOp(operator: String,
    left: Expr, right: Expr) extends Expr
    Эта иерархия включает трейт
    Expr с четырьмя подклассами, по одному для каждого вида рассматриваемых выражений. Тела всех пяти классов пусты. case-классы
    Еще одна особенность объявлений в листинге 13.1 (см. выше), которая за­
    служивает внимания, — наличие у каждого подкласса модификатора case
    Классы с таким модификатором называются case-классами. Как упоминалось в разделе 4.4, использование модификатора case заставляет компилятор
    Scala добавлять к вашему классу некоторые синтаксические удобства. Первое заключается в том, что к классу добавляется фабричный метод с именем дан­
    ного класса. Это означает, к примеру, что для создания var
    ­объекта можно применить код
    Var("x")
    :
    val v = Var("x")
    Особенно полезны фабричные методы благодаря их вложенности. Теперь код не загроможден ключевыми словами new
    , и структуру выражения можно воспринять с одного взгляда:
    val op = BinOp("+", Num(1), v)
    Второе синтаксическое удобство заключается в том, что все аргументы в спи­
    ске параметров case
    ­класса автоматически получают префикс val
    , то есть сохраняются в качестве полей:
    v.name // x op.left // Num(1.0)
    Третье удобство состоит в том, что компилятор добавляет к вашему классу
    «естественную» реализацию методов toString
    , hashCode и equals
    . Они будут заниматься подготовкой данных к выводу, их хешированием и сравнением всего дерева, состоящего из класса, и (рекурсивно) всех его аргументов.

    268 Глава 13 • Сопоставление с образцом
    Поскольку метод
    ==
    в Scala всегда передает полномочия методу equals
    , это означает, что элементы case
    ­классов всегда сравниваются структурно:
    op.toString // BinOp(+,Num(1.0),Var(x))
    op.right == Var("x") // true
    И наконец, чтобы создать измененные копии, компилятор добавляет к ва­
    шему классу метод copy
    . Он пригодится для создания нового экземпляра класса, аналогичного другому экземпляру, за исключением того, что будет отличаться одним или двумя атрибутами. Метод работает за счет использо­
    вания именованных параметров и параметров по умолчанию (см. раздел 8.8).
    Применение именованных параметров позволяет указать требуемые измене­
    ния. А для любого неуказанного параметра используется значение из старо­
    го объекта. Посмотрим в качестве примера, как можно создать операцию, похожую на op во всем, кроме того, что будет изменен параметр operator
    :
    op.copy(operator = "-") // BinOp(-,Num(1.0),Var(x))
    Все эти соглашения в качестве небольшого бонуса придают вашей работе массу удобств. Нужно просто указать модификатор case
    , и ваши классы и объекты приобретут гораздо более оснащенный вид. Они станут больше за счет создания дополнительных методов и неявного добавления поля для каждого параметра конструктора. Но самым большим преимуществом case
    ­
    классов является то, что они поддерживают сопоставления с образцом
    1
    Сопоставление с образцом
    Предположим, нужно упростить арифметические выражения только что представленных видов. Существует множество возможных правил упроще­
    ния. В качестве иллюстрации подойдут три следующих правила:
    UnOp("-", UnOp("-", е)) => е // двойное отрицание
    BinOp("+", е, Num(0)) => е // прибавление нуля
    BinOp("*", е, Num(1)) => е // умножение на единицу
    Как показано в листинге 13.2, чтобы в Scala сформировать ядро функ­
    ции упрощения с помощью сопоставления с образцом, эти правила можно взять практически в неизменном виде. Показанную в листинге функцию simplifyTop можно использовать следующим образом:
    simplifyTop(UnOp("-", UnOp("-", Var("x")))) // Var(x)
    1 case
    ­классы поддерживают сопоставление шаблонов путем создания метода из­
    влечения unapply в объекте­компаньоне.

    13 .1 . Простой пример 269
    Листинг 13.2. Функция simplifyTop, выполняющая сопоставление с образцом def simplifyTop(expr: Expr): Expr =
    expr match case UnOp("-", UnOp("-", e)) => e // двойное отрицание case BinOp("+", e, Num(0)) => e // прибавление нуля case BinOp("*", e, Num(1)) => e // умножение на единицу case _ => expr
    Правая часть simplifyTop состоит из выражения match
    , которое соответству­
    ет switch в Java, но записывается после выражения выбора. Иными словами, оно выглядит как
    выбор match { альтернативы }
    вместо switch (выбор) { альтернативы }
    Сопоставление с образцом включает последовательность альтернатив, каждая из которых начинается с ключевого слова case
    . Каждая альтернатива состоит из паттерна и одного или нескольких выражений, которые будут вычислены при соответствии паттерну. Обозначение стрелки
    =>
    отделяет паттерн от выражений.
    Выражение match вычисляется проверкой соответствия каждого из пат­
    тернов в порядке их написания. Выбирается первый же соответствующий паттерн, а также выбирается и выполняется та часть, которая следует за обозначением стрелки.
    Паттерн-константа вида
    +
    или
    1
    соответствует значениям, равным констан­
    те в случае применения метода
    ==
    . Паттерн-переменная вида e
    соответствует любому значению. Затем переменная ссылается на это же значение в правой части условия case
    . Обратите внимание: в данном примере первые три аль­
    тернативы вычисляются в e
    , то есть в переменную, связанную внутри соот­
    ветствующего паттерна. Подстановочный паттерн (
    _
    ) также соответствует любому значению, но без представления имени переменной для ссылки на это значение. Стоит отметить, что в листинге 13.2 выражение match закан­
    чивается условием case
    , которое применяется при отсутствии соответству­
    ющих паттернов и не предполагает никаких действий с выражением. Вместо этого получается просто выражение expr
    , в отношении которого и выполня­
    ется сопоставление с образцом.
    Паттерн-конструктор выглядит как
    UnOp("-",
    e)
    . Он соответствует всем значениям типа
    UnOp
    , первый аргумент которых соответствует "-"
    , а второй — e
    . Обратите внимание: аргументы конструктора сами являются

    270 Глава 13 • Сопоставление с образцом паттернами. Это позволяет составлять многоуровневые паттерны, исполь­
    зуя краткую форму записи.
    Примером может послужить следующий паттерн:
    UnOp("-", UnOp("-", e))
    Представьте попытку реализовать такую же функциональную возможность с помощью шаблона проектирования visitor
    1
    . Практически так же трудно представить реализацию такой же функциональной возможности в виде длинной последовательности инструкций, проверок соответствия типам и явного приведения типов.
    Сравнение match со switch
    Выражения match могут быть представлены в качестве общих случаев switch
    ­
    выражений в стиле Java. В Java switch
    ­выражение может быть вполне есте­
    ственно представлено в виде match
    ­выражения, где каждый паттерн является константой, а последний паттерн может быть подстановочным (который представлен в switch
    ­выражении вариантом, используемым при отсутствии других соответствий).
    И тем не менее следует учитывать три различия. Во­первых, match является
    выражением языка Scala, то есть всегда вычисляется в значение. Во­вторых, применяемые в Scala выражения альтернатив никогда не «выпадают» в сле­
    дующий вариант. В­третьих, если не найдено соответствие ни одному из паттернов, то выдается исключение
    MatchError
    . Следовательно, вам придется всегда обеспечивать охват всех возможных вариантов, даже если это будет означать вариант по умолчанию, в котором не делается ничего.
    Листинг 13.3. Сопоставление с образцом с пустым вариантом по умолчанию expr match case BinOp(op, left, right) =>
    println(s"$expr является бинарной операцией")
    case _ =>
    Пример показан в листинге 13.3. Второй необходим, поскольку без него вы­
    ражение match выдаст исключение
    MatchError для любого expr
    ­аргумента,
    1
    Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Паттерны объектно­ориентированного проектирования. — СПб.: Питер, 2020.

    13 .2 . Разновидности паттернов 271
    не являющегося
    BinOp
    . В данном примере для этого второго варианта не указан никакой код, поэтому при его срабатывании ничего не произойдет.
    Результатом в любом случае будет unit
    ­значение
    ()
    , которое также будет результатом вычисления всего выражения match
    13 .2 . Разновидности паттернов
    В предыдущем примере мы кратко описали некоторые разновидности пат­
    тернов. А теперь потратим немного времени на более подробное изучение каждого из них.
    Синтаксис паттернов довольно прост, поэтому не стоит особо переживать из­
    за него. Все паттерны выглядят точно так же, как и соответствующие им вы­
    ражения. Например, если взять иерархию из листинга 13.1, то паттерн
    Var(x)
    соответствует любому выражению, содержащему переменную, с привязкой x
    к имени переменной. Будучи использованным в качестве выражения,
    Var(x)
    с точно таким же синтаксисом воссоздает эквивалентный объект, предпола­
    гая, что идентификатор x
    уже привязан к имени переменной. В синтаксисе паттернов все прозрачно, поэтому главное, на что следует обратить внима­
    ние, — это какого вида паттерны можно применять.
    Подстановочные паттерны
    Подстановочный паттерн (
    _
    ) соответствует абсолютно любому объекту. Вы уже видели, как он используется в качестве общего паттерна, выявляющего все оставшиеся альтернативы:
    expr match case BinOp(op, left, right) =>
    s"$expr является бинарной операцией"
    case _ => // handle the default case s"Это что-то другое"
    Кроме того, подстановочные паттерны могут использоваться для игнори­
    рования тех частей объекта, которые не представляют для вас интереса.
    Например, в предыдущем примере нас не интересует, что представляют собой элементы бинарной операции, в нем лишь проверяется, является ли она бинарной. Поэтому, как показано в листинге 13.4, для элементов
    BinOp в коде также могут использоваться подстановочные паттерны.

    1   ...   25   26   27   28   29   30   31   32   ...   64


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