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

  • Листинг 13.18.

  • 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
    страница31 из 64
    1   ...   27   28   29   30   31   32   33   34   ...   64

    282 Глава 13 • Сопоставление с образцом
    Листинг 13.15. Выражение сопоставления, в котором порядок следования вариантов имеет значение def simplifyAll(expr: Expr): Expr =
    expr match case UnOp("-", UnOp("-", e)) =>
    simplifyAll(e) // '-' является своей собственной обратной величиной case BinOp("+", e, Num(0)) =>
    simplifyAll(e) // '0' нейтральный элемент для '+'
    case BinOp("*", e, Num(1)) =>
    simplifyAll(e) // '1' нейтральный элемент для '*'
    case UnOp(op, e) =>
    UnOp(op, simplifyAll(e))
    case BinOp(op, l, r) =>
    BinOp(op, simplifyAll(l), simplifyAll(r))
    case _ => expr
    Версия метода simplify
    , показанная в данном листинге, станет применять правила упрощения в любом месте выражения, а не только в его верхней части, как это сделала бы версия simplifyTop
    . Данную версию можно вы­
    вести из версии simplifyTop
    , добавив два дополнительных варианта для обычных унарных и бинарных выражений (четвертый и пятый варианты case в листинге 13.15).
    В четвертом варианте используется паттерн
    UnOp(op,
    e)
    , который соответ­
    ствует любой унарной операции. Оператор и операнд унарной операции могут быть какими угодно. Они привязаны к паттернам­переменным op и e
    соответственно. Альтернативой в данном варианте будет рекурсивное применение simplifyAll к операнду e
    с последующим перестроением той же самой унарной операции с (возможно) упрощенным операндом. Пятый вариант для
    BinOp аналогичен четвертому: он является вариантом «поймать все» для произвольных бинарных операций, который рекурсивно применяет метод упрощения к своим двум операндам.
    Важным обстоятельством в этом примере является то, что варианты «пой­
    мать все» следуют после более конкретизированных правил упрощения. Если расположить их в другом порядке, то вариант «поймать все» будет запущен вместо более конкретизированных правил. Во многих случаях компилятор будет жаловаться на такие попытки. Например, вот как выглядит выражение match
    , которое не пройдет компиляцию, поскольку первый вариант будет соответствовать всему тому, чему будет соответствовать второй вариант:
    scala> def simplifyBad(expr: Expr): Expr =
    expr match case UnOp(op, e) => UnOp(op, simplifyBad(e))
    case UnOp("-", UnOp("-", e)) => e case _ => expr

    13 .5 . Запечатанные классы 283
    def simplifyBad(expr: Expr): Expr
    4 | case UnOp("-", UnOp("-", e)) => e
    | ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
    | Unreachable case
    13 .5 . Запечатанные классы
    При написании сопоставления с образцом нужно удостовериться в том, что охвачены все возможные варианты. Иногда это можно сделать, добавив в конец match вариант по умолчанию, но данный способ применим, только когда есть вполне определенное поведение по умолчанию. А что делать, если его нет? Как узнать, что охвачены все варианты и нет опасности упустить что­либо?
    Чтобы определить пропущенные в выражении match комбинации паттернов, можно обратиться за помощью к компилятору Scala. Для этого компилятор должен иметь возможность сообщить обо всех потенциальных вариантах.
    По сути, в Scala это сделать нереально, поскольку классы могут быть опре­
    делены в любое время и в произвольных блоках компиляции. Например, ничто не помешает вам добавить к иерархии класса
    Expr пятый case
    ­класс не в том блоке компиляции, в котором определены четыре других case
    ­класса, а в другом.
    Альтернативой этому может стать превращение суперкласса ваших case
    ­
    классов в запечатанный класс. У такого запечатанного класса не может быть никаких дополнительных подклассов, кроме тех, которые определены в том же самом файле. Особую пользу из этого можно извлечь при сопо­
    ставлении с образцом, поскольку запечатанность класса будет означать, что беспокоиться придется только по поводу тех подклассов, о которых вам уже известно. Более того, будет улучшена поддержка со стороны компилятора.
    При сопоставлении с образцом case
    ­классам, являющимся наследниками запечатанного класса, компилятор в предупреждении отметит пропущенные комбинации паттернов.
    Если создается иерархия классов, предназначенная для сопоставления с образцом, то нужно предусмотреть ее запечатанность. Чтобы это сделать, просто поставьте перед классом на вершине иерархии ключевое слово sealed
    . Программисты, использующие вашу иерархию классов, при сопо­
    ставлении с образцом будут чувствовать себя уверенно. Таким образом, ключевое слово sealed зачастую выступает лицензией на сопоставление с образцом. Пример, в котором
    Expr превращается в запечатанный класс, показан в листинге 13.16.

    284 Глава 13 • Сопоставление с образцом
    Листинг 13.16. Запечатанная иерархия case-классов sealed 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
    А теперь определим сопоставление с образцом, в котором пропущены не­
    которые возможные варианты:
    def describe(e: Expr): String =
    e match case Num(_) => "число"
    case Var(_) => "переменная"
    В результате будет получена следующая ошибка компилятора:
    def describe(e: Expr): String
    2 | e match
    | ˆ
    | match may not be exhaustive.
    |
    | It would fail on pattern case: UnOp(_, _),
    | BinOp(_, _, _)
    Такая ошибка компилятора сообщает о существовании риска генерации вашим кодом исключения
    MatchError
    , поскольку некоторые возможные паттерны (
    UnOp
    ,
    BinOp
    ) не обрабатываются. Ошибка указывает на потен­
    циальный источник сбоя в ходе выполнения программы и помогает при корректировке кода.
    Но порой можно столкнуться с ситуацией, в которой компилятор при вы­
    даче ошибки проявляет излишнюю дотошность. Например, из контекста может быть известно, что показанный ранее метод describe будет приме­
    няться только к выражениям типа
    Num или
    Var
    , следовательно, исключение
    MatchError не станет генерироваться. Чтобы избавиться от ошибки, к методу можно добавить третий вариант по умолчанию:
    def describe(e: Expr): String =
    e match case Num(_) => "число"
    case Var(_) => "переменная"
    case _ => throw new RuntimeException // Не должно произойти
    Решение вполне работоспособное, однако не идеальное. Вряд ли вас обрадует принуждение добавить код, который никогда не будет выполнен (по вашему мнению), лишь для того, чтобы успокоить компилятор.

    13 .6 . Сопоставление паттерна Options 285
    Более экономной альтернативой станет добавление к селектору выражения сопоставления с образцом аннотации
    @unchecked
    . Делается это следующим образом:
    def describe(e: Expr): String =
    (e: @unchecked) match case Num(_) => "число"
    case Var(_) => "переменная"
    В общем, аннотации можно добавлять к выражению точно так же, как это дела­
    ется при добавлении типа: нужно после выражения поставить двоеточие, знак
    «собачка» и указать название аннотации. Например, в данном случае к пере­
    менной e
    добавляется аннотация
    @unchecked
    , для чего используется код e:
    @unchecked
    . Аннотация
    @unchecked имеет особое значение для сопоставления с образцом. Если выражение селектора поиска содержит данную аннотацию, то исчерпывающая проверка последующих паттернов будет подавлена.
    13 .6 . Сопоставление паттерна Options
    Вы можете использовать сопоставление шаблонов для обработки стандарт­
    ного типа
    Option в Scala. Как упоминалось в шаге 12 главы 3,
    Option может быть двух видов: это либо
    Some(x)
    , где
    x
    — реальное значение, либо
    None
    , у которого отсутствует значение.
    Необязательные значения производятся некоторыми стандартными опе­
    рациями над коллекциями Scala. Например, метод get из Scala­класса
    Map производит
    Some(значение)
    , если найдено
    значение
    , соответствующее за­
    данному ключу, или
    None
    , если заданный ключ не определен в
    Map
    ­объекте.
    Пример выглядит так:
    val capitals = Map("France" –> "Paris", "Japan" –> "Tokyo")
    capitals.get("France") // Some(Paris)
    capitals.get("North Pole") // None
    Самый распространенный способ разобрать необязательные значения — ис­
    пользовать сопоставление с образцом, например:
    def show(x: Option[String]) =
    x match case Some(s) => s case None => "?"
    show(capitals.get("Japan")) // Tokyo show(capitals.get("France")) // Paris show(capitals.get("North Pole")) // ?

    286 Глава 13 • Сопоставление с образцом
    Тип
    Option применяется в программах на языке Scala довольно часто. Его использование можно сравнить с доминирующей в Java идиомой null
    , по­
    казывающей отсутствие значения. Например, метод get из java.util.HashMap возвращает либо значение, сохраненное в
    HashMap
    , либо null
    , если значение не было найдено. В Java такой подход работает, но, применяя его, легко до­
    пустить ошибку, поскольку на практике довольно трудно отследить, каким переменным в программе разрешено иметь значение null
    В случае, когда переменной разрешено иметь значение null
    , вы должны вспомнить о ее проверке на наличие этого значения при каждом исполь­
    зовании. Если забыть выполнить эту проверку, то появится вероятность генерации в ходе выполнения программы исключений
    NullPointerException
    Подобные исключения могут генерероваться довольно редко, поэтому с вы­
    явлением ошибки при тестировании могут возникнуть затруднения. В Scala такой подход вообще не сработает, поскольку этот язык позволяет сохранять типы значений в хеш­отображениях, а null не является допустимым элемен­
    том для типов значений. Например,
    HashMap[Int,
    Int]
    не может вернуть null
    , чтобы обозначить отсутствие элемента.
    Вместо этого в Scala для указания необязательного значения применяется тип
    Option
    . Такой способ имеет ряд преимуществ по сравнению с используемым в подходе null
    . Во­первых, тем, кто читает код, намного понятнее, что перемен­
    ная, типом которой является
    Option[String]
    , — необязательная переменная
    String
    , а не переменная типа
    String
    , которая иногда может иметь значение null
    . Во­вторых, что более важно, рассмотренные ранее ошибки програм­
    мирования, связанные с использованием переменной со значением null без предварительной проверки ее на null
    , превращаются в Scala в ошибку типа.
    Если переменная имеет тип
    Option[String]
    , то при попытке ее использования в качестве строки ваша программа на Scala не пройдет компиляцию.
    13 .7 . Паттерны повсюду
    Паттерны можно использовать не только в отдельно взятых match
    ­выра­
    жениях, но и во многих других местах программы на языке Scala. Рассмотрим несколько подобных мест применения паттернов.
    Паттерны в определениях переменных
    При определении val
    ­ или var
    ­переменной вместо простых идентификато­
    ров можно использовать паттерны. Например, можно, как показано в ли­

    13 .7 . Паттерны повсюду 287
    стинге 13.17, разобрать кортеж и присвоить каждую его часть собственной переменной.
    Листинг 13.17. Определение нескольких переменных с помощью одного присваивания scala> val myTuple = (123, "abc")
    val myTuple: (Int, String) = (123,abc)
    scala> val (number, string) = myTuple val number: Int = 123
    val string: String = abc
    Особенно полезной эта конструкция может быть при работе с case
    ­классами.
    Если точно известен case
    ­класс, с которым ведется работа, то вы можете разобрать его с помощью паттерна. Пример выглядит следующим образом:
    scala> val exp = new BinOp("*", Num(5), Num(1))
    val exp: BinOp = BinOp(*,Num(5.0),Num(1.0))
    scala> val BinOp(op, left, right) = exp val op: String = *
    val left: Expr = Num(5.0)
    val right: Expr = Num(1.0)
    Последовательности вариантов в качестве частично примененных функций
    Последовательность вариантов (то есть альтернатив), заключенную в фигур­
    ные скобки, можно задействовать везде, где может использоваться функцио­
    нальный литерал. По сути, последовательность вариантов и есть функцио­
    нальный литерал, только более универсальный. Вместо единственной точки входа и списка параметров последовательность вариантов имеет несколько точек входа, каждой из которых присущ собственный список параметров.
    Каждый вариант является точкой входа в функцию, а параметры указы­
    ваются с помощью паттерна. Тело каждой точки входа — правосторонняя часть варианта.
    Простой пример выглядит следующим образом:
    val withDefault: Option[Int] => Int =
    case Some(x) => x case None => 0
    В теле этой функции имеется два варианта. Первый соответствует
    Some и возвращает число, находящееся внутри
    Some
    . Второй соответствует

    288 Глава 13 • Сопоставление с образцом
    None и возвращает стандартное значение
    0
    . А вот как используется данная функция:
    withDefault(Some(10)) // 10
    withDefault(None) // 0
    Такая возможность особенно полезна для библиотеки акторов Akka, посколь­
    ку позволяет определить ее метод receive в виде серии вариантов:
    var sum = 0
    def receive =
    case Data(byte) =>
    sum += byte case GetChecksum(requester) =>
    val checksum =

    (sum & 0xFF) + 1
    requester ! checksum
    Кроме того, стоит упомянуть еще одно общее правило: последовательность вариантов дает вам частично примененную функцию. Если применить такую функцию в отношении не поддерживаемого ею значения, то она сгенерирует исключение времени выполнения. Например, ниже показана частично при­
    мененная функция, которая возвращает второй элемент списка, состоящего из целых чисел:
    val second: List[Int] => Int =
    case x :: y :: _ => y
    При компиляции этого кода компилятор вполне резонно выведет предупре­
    ждение о том, что сопоставление с образцом не охватывает все возможные варианты:
    2 | case x :: y :: _ => y
    | ˆ
    | match may not be exhaustive.
    |
    | It would fail on pattern case: List(_), Nil
    Функция справится со своей задачей, если ей передать список, состоящий из трех элементов, но не станет работать при передаче пустого списка:
    scala> second(List(5, 6, 7))
    val res24: Int = 6
    scala> second(List())
    scala.MatchError: List() (of class Nil$)
    at rs$line$10$.$init$$$anonfun$1(rs$line$10:2)
    at rs$line$12$.(rs$line$12:1)

    13 .7 . Паттерны повсюду 289
    Если нужно проверить, определена ли частично примененная функция, то сначала следует сообщить компилятору: вы знаете, что работаете с ча­
    стично примененными функциями. Тип
    List[Int]
    =>
    Int включает все функции, получающие из целочисленных списков целочисленные значе­
    ния независимо от того, частично они применяются или нет. Тип, который включает только частично примененные функции, которые получают из целочисленных списков целочисленные значения, записывается в виде
    PartialFunction[List[Int],Int]
    . Ниже представлен еще один вариант функции second
    , определенной с типом частично примененной функции:
    val second: PartialFunction[List[Int],Int] =
    case x :: y :: _ => y
    У частично примененных функций есть метод isDefinedAt
    , который может использоваться для тестирования того, определена ли функция в отношении конкретного значения. В данном случае функция определена для любого списка, состоящего по крайней мере из двух элементов:
    second.isDefinedAt(List(5,6,7)) // true second.isDefinedAt(List()) // false
    Типичным образчиком частично примененной функции может послужить функциональный литерал сопоставления с образцом, подобный представлен­
    ному в предыдущем примере. Фактически такое выражение преобразуется компилятором Scala в частично примененную функцию с помощью двойного преобразования паттернов: один раз для реализации реальной функции, а второй — для проверки того, определена ли функция.
    Например, функциональный литерал
    {
    case x
    ::
    y
    ::
    _
    =>
    y
    }
    преобразуется в следующее значение частично примененной функции:
    new PartialFunction[List[Int], Int]:
    def apply(xs: List[Int]) =
    xs match case x :: y :: _ => y def isDefinedAt(xs: List[Int]) =
    xs match case x :: y :: _ => true case _ => false
    Это преобразование осуществляется в том случае, когда в качестве объяв­
    ляемого типа функционального литерала выступает
    PartialFunction
    . Если объявляемый тип — просто
    Function1
    или не указан, функциональный ли­
    терал вместо этого преобразуется в полноценную функцию.

    290 Глава 13 • Сопоставление с образцом
    Вообще­то, полноценными функциями нужно пробовать пользоваться везде, где только можно, поскольку использование частично примененных функций допускает возникновение ошибок времени выполнения, устра­
    нить которые компилятор вам не может помочь. Но иногда частично при­
    мененные функции приносят реальную пользу. Вам следует позаботиться о том, чтобы этим функциям не было предоставлено необрабатываемое значение. Как вариант, вы можете задействовать фреймворк, который до­
    пускает использование частично примененных функций и поэтому всегда перед вызовом функции выполняет проверку функцией isDefinedAt
    . По­
    следнее проиллюстрировано приведенным ранее примером метода receive
    , где результатом выступает частично примененная функция с определе­
    нием, данным в точности для тех сообщений, которые нужно обработать вызывающему коду.
    Паттерны в выражениях for
    Паттерны, как показано ниже, в листинге 13.18, можно использовать также в выражениях for
    . Это выражение извлекает все пары «ключ — значение» из отображения capitals
    (столицы). Каждая пара соответствует паттер­
    ну
    (country,
    city)
    (страна, город), который определяет две переменные: country и city
    Листинг 13.18. Выражение for с паттерном-кортежем for (country, city) <- capitals yield s"Столицей $country является $city"
    //
    // List(Столицей France является Paris,
    // Столицей Japan является Tokyo)
    Паттерн пар, показанный в данном листинге, интересен, поскольку сопостав­
    ление с ним никогда не даст сбой. Конечно, capitals выдает последователь­
    ность пар, следовательно, можно быть уверенными, что каждая сгенериро­
    ванная пара может соответствовать паттерну пар.
    Но с равной долей вероятности возможно, что паттерн не будет соответ­
    ствовать сгенерированному значению. Именно такой случай показан в ли­
    стинге 13.19.
    1   ...   27   28   29   30   31   32   33   34   ...   64


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