Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 730
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
1 ... 26 27 28 29 30 31 32 33 ... 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
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$.
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
Листинг 13.19. Отбор элементов списка, соответствующих паттерну val results = List(Some("apple"), None, Some("orange"))
for Some(fruit) <- results yield fruit
// List(apple, orange)
13 .8 . Большой пример 291
В этом примере показано, что сгенерированные значения, не соответству
ющие паттерну, отбрасываются. Так, второй элемент
None в получившемся списке не соответствует паттерну
Some(fruit)
, поэтому отсутствует в ре
зультате.
13 .8 . Большой пример
После изучения различных форм паттернов может быть интересно посмо
треть на их применение в более существенном примере. Предлагаемая задача заключается в написании класса, форматирующего выражения, который вы
водит арифметическое выражение в двумерной разметке. Такое выражение деления, как x
/
(x
+
1)
, должно быть выведено вертикально — с числителем, показанным над знаменателем:
x
----- x + 1
В качестве еще одного примера ниже в двумерной разметке показано вы
ражение
((a
/
(b
*
c)
+
1
/
n)
/
3)
:
a 1
----- + - b * c n
---------
3
Исходя из этих примеров, можно прийти к выводу, что манипулированием разметкой должен заняться класс — назовем его
ExprFormatter
, поэтому имеет смысл задействовать библиотеку разметки, разработанную в главе 10.
Мы также используем семейство case
классов
Expr
, ранее уже встречавшееся в данной главе, и поместим в именованные пакеты как библиотеку разметки из главы 10, так и средство форматирования выражений. Полный код этого примера будет показан ниже, в листингах 13.20 и 13.21.
Сначала полезно будет сосредоточиться на горизонтальной разметке. Струк
турированное выражение
BinOp("+",
BinOp("*",
BinOp("+", Var("x"), Var("y")),
Var("z")),
Num(1))
292 Глава 13 • Сопоставление с образцом должно привести к выводу
(x
+
y)
*
z
+
1
. Обратите внимание на обязатель
ность круглых скобок вокруг выражения x
+
y и их необязательность вокруг выражения
(x
+
y)
*
z
. Чтобы разметка получилась максимально разборчи
вой, следует стремиться к отказу от избыточных круглых скобок и обеспе
чить наличие всех обязательных.
Чтобы узнать, куда ставить круглые скобки, код должен быть в курсе от
носительной приоритетности каждого оператора; как следствие, неплохо было бы сначала отрегулировать именно этот вопрос. Относительную прио
ритетность можно выразить непосредственно в виде литерала отображения следующей формы:
Map(
"|" –> 0, "||" –> 0,
"&" –> 1, "&&" –> 1, ...
)
Конечно, вам потребуется предварительно выполнить определенный объем вычислительной работы для назначения приоритетов. Удобнее будет просто определить группы операторов с нарастающим уровнем приоритета, а затем, исходя из этого, вычислить приоритет каждого оператора. Соответствующий код показан в листинге 13.20.
Листинг 13.20. Верхняя половина средства форматирования выражений package org.stairwaybook.expr import org.stairwaybook.layout.Element.elem sealed abstract class 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 class ExprFormatter:
// Содержит операторы в группах с нарастающей степенью приоритетности private val opGroups =
Vector(
Set("|", "||"),
Set("&", "&&"),
Set("ˆ"),
Set("==", "!="),
Set("<", "<=", ">", ">="),
Set("+", "-"),
Set("*", "%")
)
13 .8 . Большой пример 293
// Отображение операторов на их степень приоритетности private val precedence = {
val assocs =
for i <- 0 until opGroups.length op <- opGroups(i)
yield op –> i assocs.toMap
}
private val unaryPrecedence = opGroups.length private val fractionPrecedence = -1
// продолжение в листинге 13.21...
Переменная precedence
— отображение операторов на уровень их приорите
та, представленный целыми числами, начинающимися с нуля. Приоритет вы
числяется с использованием выражения for с двумя генераторами. Первый выдает каждый индекс i
вектора opGroups
, второй — каждый оператор op
, находящийся в opGroups(i)
. Для каждого такого оператора выражение for выдает привязку оператора op к его индексу i
. В результате приоритетность оператора берется из его относительной позиции в векторе.
Листинг 13.21. Нижняя половина средства форматирования выражений
// ...продолжение, начало в листинге 13.20
import org.stairwaybook.layout.Element private def format(e: Expr, enclPrec: Int): Element =
e match case Var(name) =>
elem(name)
case Num(number) =>
def stripDot(s: String) =
if s endsWith ".0" then s.substring(0, s.length - 2)
else s elem(stripDot(number.toString))
case UnOp(op, arg) =>
elem(op) beside format(arg, unaryPrecedence)
case BinOp("/", left, right) =>
val top = format(left, fractionPrecedence)
val bot = format(right, fractionPrecedence)
val line = elem('-', top.width.max(bot.width), 1)
val frac = top above line above bot if enclPrec != fractionPrecedence then frac
294 Глава 13 • Сопоставление с образцом else elem(" ") beside frac beside elem(" ")
case BinOp(op, left, right) =>
val opPrec = precedence(op)
val l = format(left, opPrec)
val r = format(right, opPrec + 1)
val oper = l beside elem(" " + op + " ") beside r if enclPrec <= opPrec then oper else elem("(") beside oper beside elem(")")
end match def format(e: Expr): Element = format(e, 0)
end ExprFormatter
Привязки записываются с помощью инфиксной стрелки, например op
–>
i
До сих пор они были показаны только как часть конструкций отображений, но и сами по себе они имеют значение. Фактически привязка op
–>
i есть не что иное, как пара
(op,
i)
Теперь, зафиксировав уровень приоритета всех бинарных операторов, за исключением
/
, имеет смысл обобщить данную концепцию, охватив также унарные операторы. Уровень приоритета унарного оператора выше, чем у любого бинарного оператора. Поэтому для переменной unaryPrecedence
, показанной в листинге 13.20, устанавливается значение длины вектора opGroups на единицу большее, чем уровень приоритета операторов
*
и
%
Прио ритет оператора деления рассматривается не так, как у других опе
раторов, поскольку для дробей используется вертикальная разметка. Но, конечно же, было бы удобно присвоить оператору деления специальное значение уровня приоритета
-1
, поэтому переменная fractionPrecedence будет инициализирована значением
-1
, как было показано в листинге 13.20.
После такой подготовительной работы можно приступать к написанию основного метода format
. Этот метод получает два аргумента: выражение e
, имеющее тип
Expr
, и уровень приоритета enclPrec того оператора, который непосредственно заключен в данное выражение. (Если в выражении нет ни
какого оператора, то значение enclPrec должно быть нулевым.) Метод выда
ет элемент разметки, представленный в виде двумерного массива символов.
В листинге 13.21 показана остальная часть класса
ExprFormatter
, включа
ющая два метода. Первый — приватный метод format
— выполняет основную работу по форматированию выражений. Второй, который также называется format
, представляет собой единственный публичный метод в библиотеке, получающий выражение для форматирования. Приватный метод format
13 .8 . Большой пример 295
проделывает свою работу, выполняя сопоставление с образцом по разно
видностям выражения. У выражения match есть пять вариантов, каждый из которых будет рассмотрен отдельно.
Первый вариант имеет следующий вид:
case Var(name) =>
elem(name)
Если выражение является переменной, то результатом станет элемент, сфор
мированный из имени переменной.
Второй вариант выглядит так:
case Num(number) =>
def stripDot(s: String) =
if s endsWith ".0" then s.substring(0, s.length - 2)
else s elem(stripDot(number.toString))
Если выражение является числом, то результатом станет элемент, сформи
рованный из значения числа. Функция stripDot очистит изображение числа с плавающей точкой, удалив из строки любой суффикс вида ".0"
А вот как выглядит третий вариант:
case UnOp(op, arg) =>
elem(op) beside format(arg, unaryPrecedence)
Если выражение представляет собой унарную операцию
UnOp(op,
arg)
, то результат будет сформирован из операции op и результата форматирования аргумента arg с самым высоким из возможных уровнем приоритета, име
ющимся в данном окружении
1
. Это означает, что, если аргумент arg является бинарной операцией (но не делением), то всегда будет отображаться в круг
лых скобках.
Четвертый вариант представлен следующим кодом:
case BinOp("/", left, right) =>
val top = format(left, fractionPrecedence)
val bot = format(right, fractionPrecedence)
val line = elem('-', top.width.max(bot.width), 1)
val frac = top above line above bot
1
Значение unaryPrecedence является самым высоким из возможных приоритетом, поскольку ему было присвоено значение, на единицу превышающее значения прио
ритета операторов
*
и
%
296 Глава 13 • Сопоставление с образцом if enclPrec != fractionPrecedence then frac else elem(" ") beside frac beside elem(" ")
Если выражение имеет вид дроби, то промежуточный результат frac фор
мируется путем помещения отформатированных операндов left и right друг над другом с разделительным элементом в виде горизонтальной линии.
Ширина горизонтальной линии равна максимальной ширине отформатиро
ванных операндов. Промежуточный результат становится окончательным, если только дробь сама по себе не появляется в виде аргумента еще одной функции. В последнем случае по обе стороны frac добавляется по пробелу.
Чтобы понять, зачем это делается, рассмотрим выражение
(a
/
b)
/
c
Без коррекции по ширине при форматировании этого выражения получится следующая картинка:
a
- b
- c
Вполне очевидна проблема с разметкой: непонятно, где именно находится дробная черта верхнего уровня. Показанное ранее выражение может означать либо
(a
/
b)
/
c
, либо a
/
(b
/
c)
. Чтобы устранить неоднозначность, с обеих сторон разметки вложенной дроби a
/
b нужно добавить пробелы.
Тогда разметка станет однозначной:
a
- b
--- c
Пятый и последний вариант выглядит следующим образом:
case BinOp(op, left, right) =>
val opPrec = precedence(op)
val l = format(left, opPrec)
val r = format(right, opPrec + 1)
val oper = l beside elem(" " + op + " ") beside r if enclPrec <= opPrec then oper else elem("(") beside oper beside elem(")")
Этот вариант применяется ко всем остальным бинарным операциям. Он указан после варианта, который начинается со следующего кода:
case BinOp("/", left, right) => ...