Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 738
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
272 Глава 13 • Сопоставление с образцом
Листинг 13.4. Сопоставление с образцом с использованием подстановочных паттернов expr match case BinOp(_, _, _) => s"$expr является бинарной операцией"
case _ => "Это что-то другое"
Паттерны-константы
Паттернконстанта соответствует только самому себе. В качестве константы может использоваться любой литерал. Например, паттернамиконстантами являются
5
, true и "hello"
. В качестве константы может использоваться и любой val
или объектодиночка. Так, объектодиночка
Nil является паттерном, соответствующим только пустому списку. Некоторые примеры паттерновконстант показаны в листинге 13.5. Вот как сопоставление с об
разцом выглядит в действии.
Листинг 13.5. Сопоставление с образцом с использованием паттернов-констант def describe(x: Any) =
x match case 5 => "пять"
case true => "правда"
case "hello" => "привет!"
case Nil => "пустой список"
case _ => "что-то другое"
describe(5) // пять describe(true) // правда describe("hello") // привет!
describe(Nil) // пустой список describe(List(1,2,3)) // что-то другое
Патерны-переменные
Паттернпеременная соответствует любому объекту точно так же, как под
становочный паттерн, но в отличие от него Scala привязывает переменную к объекту. Затем с помощью этой переменной можно в дальнейшем воз
действовать на объект. Например, в листинге 13.6 показано сопоставление с образцом, имеющее специальный вариант для нуля и общий вариант для всех остальных значений. В общем варианте используется паттернпере
менная, и поэтому у него есть имя для переменной независимо от того, что это на самом деле.
13 .2 . Разновидности паттернов 273
Листинг 13.6. Сопоставление с образцом с использование паттерна-переменной expr match case 0 => "нуль"
case somethingElse => s"не нуль $somethingElse"
Переменная или константа?
У паттерновконстант могут быть символические имена. Вы уже это виде
ли, когда в качестве образца использовался
Nil
. А вот похожий пример, где в сопоставлении с образцом задействуются константы
E
(2,718 28...) и
Pi
(3,141 59...):
scala> import math.{E, Pi}
import math.{E, Pi}
scala> E match case Pi => s"математический казус? Pi = $Pi"
case _ => "OK"
val res0: String = OK
Как и ожидалось, значение
E
не равно значению
Pi
, поэтому вариант «мате
матический казус» не выбирается.
А как компилятор Scala распознает, что
Pi
— это константа, импортированная из scala.math
, а не переменная, обозначающая само значение селектора?
Во избежание путаницы в Scala действует простое лексическое правило: обычное имя, начинающееся с буквы в нижнем регистре, считается перемен
ной паттерна, а все другие ссылки считаются константами. Чтобы заметить разницу, создайте для pi псевдоним с первой буквой, указанной в нижнем регистре, и попробуйте в работе следующий код:
scala> val pi = math.Pi pi: Double = 3.141592653589793
scala> E match case pi => s"математический казус? Pi = $pi"
val res1: String = математический казус? Pi = 2.718281828459045
Здесь компилятор даже не позволит вам добавить вариант по умолчанию.
Поскольку pi
— паттернпеременная, то будет соответствовать всем вводи
мым данным, поэтому до следующих вариантов дело просто не дойдет:
scala> E match case pi => s"математический казус? Pi = $pi"
274 Глава 13 • Сопоставление с образцом case _ => "OK"
val res2: String = математический казус? Pi = 2.718281828459045 3 | case _ => "OK"
| ˆ
| Unreachable case
Но при необходимости для паттернаконстанты можно задействовать имя, начинающееся с буквы в нижнем регистре; для этого придется восполь
зоваться одним из двух приемов. Если константа является полем какого
нибудь объекта, то перед ней можно поставить префиксклассификатор.
Например, pi
— паттернпеременная, а this.pi или obj.pi
— константы, несмотря на то что их имена начинаются с букв в нижнем регистре. Если это не сработает (поскольку, скажем, pi
— локальная переменная), то как вариант можно будет заключить имя переменной в обратные кавычки.
Например,
`pi`
будет опять восприниматься как константа, а не как пере
менная:
scala> E match case `pi` => s"математический казус? Pi = $pi"
case _ => "OK"
res4: String = OK
Как вы, наверное, заметили, использование для идентификаторов в Scala синтаксиса с обратными кавычками во избежание в коде необычных обсто
ятельств преследует две цели. Здесь показано, что этот синтаксис может применяться для рассмотрения идентификатора с именем, начинающимся с буквы в нижнем регистре, в качестве константы при сопоставлении с об
разцом. Ранее, в разделе 6.10, было показано, что этот синтаксис может использоваться также для трактовки ключевого слова в качестве обычного идентификатора. Например, в выражении writingThread.`yield`()
слово yield трактуется как идентификатор, а не ключевое слово.
Паттерны-конструкторы
Реальная эффективность сопоставления с образцом проявляется имен
но в конструкторах. Паттернконструктор выглядит как
BinOp("+",
e,
Num(0))
. Он состоит из имени (
BinOp
), после которого в круглых скобках стоят несколько образцов:
"+"
, e
и
Num(0)
. При условии, что имя обознача
ет case
класс, такой паттерн показывает следующее: сначала проверяется принадлежность элемента к названному case
классу, а затем соответствие
13 .2 . Разновидности паттернов 275
параметров конструктора объекта предоставленным дополнительным пат
тернам.
Эти дополнительные паттерны означают, что в паттернах Scala поддержи
ваются глубкие сопоставления (deep matches). Такой паттерн проверяет не только предоставленный объект верхнего уровня, но и его содержимое на соответствие следующим паттернам. Дополнительные паттерны сами по себе могут быть паттернамиконструкторами, поэтому их можно использовать для проверки объекта произвольной глубины. Например, паттерн, показан
ный в листинге 13.7, проверяет, что объект верхнего уровня относится к типу
BinOp
, третьим параметром его конструктора является число
Num и значение поля этого числа —
0
. Весь паттерн умещается в одну строку кода, хотя вы
полняет проверку на глубину в три уровня.
Листинг 13.7. Сопоставление с образцом с использованием паттерна-конструктора expr match case BinOp("+", e, Num(0)) => "глубокое соответствие"
case _ => ""
Паттерны-последовательности case
классы можно сопоставлять с такими типами последовательностей, как
List или
Array
. Однако теперь в паттерне вы можете указать любое ко
личество элементов, пользуясь тем же синтаксисом. В листинге 13.8 показан шаблон для проверки трехэлементного списка, начинающегося с нуля.
Листинг 13.8. Паттерн-последовательность фиксированной длины xs match case List(0, _, _) => "соответствие найдено"
case _ => ""
Если нужно сопоставить с последовательностью, не указывая ее длину, то в качестве последнего элемента паттернапоследовательности можно указать образец
_*
. Он имеет весьма забавный вид и соответствует любому количеству элементов внутри последовательности, включая ноль элементов.
В листинге 13.9 показан пример, соответствующий любому списку, который начинается с нуля, независимо от длины этого списка.
Листинг 13.9. Паттерн-последовательность произвольной длины xs match case List(0, _, _) => " соответствие найдено "
case _ => ""
276 Глава 13 • Сопоставление с образцом
Паттерны-кортежи
Можно выполнять и сопоставление с кортежами. Паттерн вида
(a,
b,
c)
соответствует произвольному трехэлементному кортежу. Пример показан в листинге 13.10.
Если загрузить показанный в листинге 13.10 метод tupleDemo в интерпре
татор и передать ему кортеж из трех элементов, то получится следующая картина.
Листинг 13.10. Сопоставление с образцом с использованием паттерна-кортежа def tupleDemo(obj: Any) =
obj match case (a, b, c) => s"matched $a$b$c"
case _ => ""
tupleDemo(("a ", 3, "-tuple")) // соответствует a 3-tuple
Типизированные паттерны
Типизированный паттерн (typed pattern) можно использовать в качестве удобного заменителя для проверок типов и приведения типов. Пример по
казан в листинге 13.11.
Листинг 13.11. Сопоставление с образцом с использованием типизированных паттернов def generalSize(x: Any) =
x match case s: String => s.length case m: Map[_, _] => m.size case _ => -1
А вот несколько примеров использования generalSize в интерпретаторе
Scala:
generalSize("abc") // 3
generalSize(Map(1 –> 'a', 2 –> 'b')) // 2
generalSize(math.Pi) // -1
Метод generalSize возвращает размер или длину объектов различных типов.
Типом его аргумента является
Any
, поэтому им может быть любое значение.
Если в качестве типа аргумента выступает
String
, то метод возвращает длину строки. Образец s:
String является типизированным паттерном и соответ
13 .2 . Разновидности паттернов 277
ствует каждому (ненулевому) экземпляру класса
String
. Затем на эту строку ссылается паттернпеременная s
Заметьте: даже притом что s
и x
ссылаются на одно и то же значение, типом x
является
Any
, а типом s
является
String
. Поэтому в альтернативном выра
жении, соответствующем паттерну, можно воспользоваться кодом s.length
, но нельзя — кодом x.length
, поскольку в типе
Any отсутствует член length
Эквивалентный, но более многословный способ достичь такого же результата сопоставления с типизированным образцом — использовать проверку типа с его последующим приведением. В Scala для этого применяется не такой синтаксис, как в Java. К примеру, чтобы проверить, относится ли выражение expr к типу
String
, используется такой код:
expr.isInstanceOf[String]
Для приведения того же выражения к типу
String используется код expr.asInstanceOf[String]
Применяя проверку и приведение типа, можно переписать первый вариант предыдущего match
выражения, получив код, показанный в листинге 13.12.
Листинг 13.12. Использование isInstanceOf и asInstanceOf (плохой стиль)
if x.isInstanceOf[String] then val s = x.asInstanceOf[String]
s.length else ...
Операторы isInstanceOf и asInstanceOf считаются предопределенными методами класса
Any
, получающими параметр типа в квадратных скобках.
Фактически x.asInstanceOf[String]
— частный случай вызова метода с явно заданным параметром типа
String
Как вы уже заметили, написание проверок и приведений типов в Scala страдает излишним многословием. Сделано это намеренно, поскольку по
добная практика не приветствуется. Как правило, лучше воспользоваться сопоставлением с типизированным образцом. В частности, подобный подход будет оправдан, если нужно выполнить две операции: проверку типа и его приведение, так как обе они будут сведены к единственному сопоставлению.
Второй вариант match
выражения в листинге 13.11 содержит типизи
рованный паттерн m:
Map[_,
_]
. Он соответствует любому значению, явля ющемуся отображением какихлибо произвольных типов ключа
278 Глава 13 • Сопоставление с образцом и значения, и позволяет m
ссылаться на это значение. Поэтому m.size имеет правильный тип и возвращает размер отображения. Знаки подчеркивания в типизированном паттерне
1
подобны таким же знакам в подстановочных паттернах. Вместо них можно указывать переменные типа с символами в нижнем регистре.
Приписывание типов
Приведения по своей сути небезопасны. Например, даже если у ком
пилятора достаточно информации, чтобы определить, что приведение из
Int в
String не сработает во время выполнения, оно все равно ком
пилируется (и завершается сбоем во время выполнения):
3.asInstanceOf[String]
// java.lang.ClassCastException: java.lang.Integer
// не может быть приведен к java.lang.String
Безопасной альтернативой приведения является приписывание ти
пов: размещение двоеточия и типа после переменной или выражения.
Приписывание типов безопасно, потому что любое неправильное приписывание, например приписывание
Int к типу
String
, приведет к ошибке компилятора, а не к исключению во время выполнения:
scala> 3: String // ': String' — приписывание типов
1 |3: String
|ˆ
|Found: (3 : Int)
|Required: String
Приписывание типа будет компилироваться только в двух случаях.
Вопервых, вы можете использовать его для расширения типа до од
ного из его супертипов. Например:
scala> Var("x"): Expr // Expr — супертип Var val res0: Expr = Var(x)
Вовторых, вы можете использовать его для неявного преобразования одного типа в другой, например для неявного преобразования
Int в
Long
:
scala> 3: Long val res1: Long = 3 1
В типизированном паттерне m:
Map[_,
_],
часть "Map[_,
_]"
называется паттерном типа.
13 .2 . Разновидности паттернов 279
Затирание типов
А можно ли также проверять на отображение с конкретными типами элемен
тов? Это пригодилось бы, скажем, для проверки того, является ли заданное значение отображением типа
Int на тип
Int
. Попробуем:
scala> def isIntIntMap(x: Any) =
x match case m: Map[Int, Int] => true case _ => false def isIntIntMap(x: Any): Boolean
3 | case m: Map[Int, Int] => true
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| the type test for Map[Int, Int] cannot be
| checked at runtime
В Scala точно так же, как и в Java, используется модель затирания обобщен
ных типов. Это значит, в ходе выполнения программы никакая информация об аргументах типов не сохраняется. Следовательно, способов определить в ходе выполнения программы, создавался ли заданный
Map
объект с двумя
Int
аргументами, а не с аргументами других типов, не существует. Система может лишь определить, что значение является отображением (
Map
) неких произвольных параметров типа. Убедиться в таком поведении можно, при
менив isIntIntMap к различным экземплярам класса
Map
:
isIntIntMap(Map(1 –> 1)) // true isIntIntMap(Map("abc" –> "abc")) // true
Первое применение возвращает true
, что выглядит вполне корректно, но второе тоже возвращает true
, и это может оказаться сюрпризом. Чтобы оповестить вас о возможном непонятном поведении программы в ходе ее выполнения, компилятор выдает предупреждение о том, что не контролирует это поведение, похожее на показанные ранее.
Единственное исключение из правила затирания — массивы, поскольку в Java, а также в Scala они обрабатываются особым образом. Тип элемента массива сохраняется вместе со значением массива, поэтому к нему можно применить сопоставление с образцом. Пример выглядит так:
def isStringArray(x: Any) =
x match case a: Array[String] => "yes"
case _ => "no"
isStringArray(Array("abc")) // да isStringArray(Array(1, 2, 3)) // нет
280 Глава 13 • Сопоставление с образцом
Привязка переменной
Кроме использования отдельно взятого паттернапеременной, можно так
же добавить переменную к любому другому паттерну. Нужно указать имя переменной, знак «собачки» (
@
), а затем паттерн. Это даст вам паттерн с привязанной переменной, то есть паттерн для выполнения обычного со
поставления с образцом с возможностью в случае совпадения присвоить переменной соответствующий объект, как и при использовании обычного паттернапеременной.
В качестве примера в листинге 13.13 показано сопоставление с образцом — поиск операции получения абсолютного значения, применяемой в строке дважды. Такое выражение можно упростить, однократно получив абсолют
ное значение.
Листинг 13.13. Паттерн с привязкой переменной (посредством использования знака @)
expr match case UnOp("abs", e @ UnOp("abs", _)) => e case _ =>
Пример, показанный в данном листинге, включает паттерн с привязкой переменной, где в качестве переменной выступает e
, а в качестве паттерна —
UnOp("abs",
_)
. Если будет найдено соответствие всему паттерну, то часть, которая соответствует
UnOp("abs",
_)
, станет доступна как значение пере
менной e
. Результатом варианта будет просто e
, поскольку e
имеет значение, равное expr
, но с меньшим на единицу количеством операций получения абсолютного значения.
13 .3 . Ограждение образца
Иногда синтаксическое сопоставление с образцом является недостаточно точным. Предположим, перед вами стоит задача сформулировать правило упрощения, заменяющее выражение сложения с двумя одинаковыми операн
дами, такое как e
+
e
, умножением на два, например e
*
2
. На языке деревьев
Expr выражение вида
BinOp("+", Var("x"), Var("x"))
этим правилом будет превращено в
BinOp("*", Var("x"), Num(2))
13 .4 . Наложение паттернов 281
Правило можно попробовать выразить следующим образом:
scala> def simplifyAdd(e: Expr) =
e match case BinOp("+", x, x) => BinOp("*", x, Num(2))
case _ => e
3 | case BinOp("+", x, x) => BinOp("*", x, Num(2))
| ˆ
| duplicate pattern variable: x
Попытка будет неудачной, поскольку в Scala паттерны должны быть линей-
ными: паттернпеременная может появляться в образце только один раз.
Но, как показано в листинге 13.14, соответствие можно переформулировать с помощью ограничителя паттернов (pattern guard).
Листинг 13.14. Сопоставление с образцом с применением ограждения паттернов def simplifyAdd(e: Expr) =
e match case BinOp("+", x, y) if x == y =>
BinOp("*", x, Num(2))
case _ => e
Ограждение паттерна указывается после образца и начинается с ключевого слова if
. В качестве ограждения может использоваться произвольное бу
лево выражение, которое обычно ссылается на переменные в образце. При наличии ограждения паттернов соответствие считается найденным, только если ограждение вычисляется в true
. Таким образом, первый вариант по
казанного ранее кода соответствует только бинарным операциям, имеющим два одинаковых операнда.
А вот как выглядят некоторые другие огражденные паттерны:
// соответствует только положительным целым числам case n: Int if 0 < n => ...
// соответствует только строкам, начинающимся с буквы 'a'
case s: String if s(0) == 'a' => ...
13 .4 . Наложение паттернов
Паттерны применяются в порядке их указания. Версия метода simplify
, по
казанная в листинге 13.15, представляет собой пример, в котором порядок следования вариантов имеет значение.
Листинг 13.4. Сопоставление с образцом с использованием подстановочных паттернов expr match case BinOp(_, _, _) => s"$expr является бинарной операцией"
case _ => "Это что-то другое"
Паттерны-константы
Паттернконстанта соответствует только самому себе. В качестве константы может использоваться любой литерал. Например, паттернамиконстантами являются
5
, true и "hello"
. В качестве константы может использоваться и любой val
или объектодиночка. Так, объектодиночка
Nil является паттерном, соответствующим только пустому списку. Некоторые примеры паттерновконстант показаны в листинге 13.5. Вот как сопоставление с об
разцом выглядит в действии.
Листинг 13.5. Сопоставление с образцом с использованием паттернов-констант def describe(x: Any) =
x match case 5 => "пять"
case true => "правда"
case "hello" => "привет!"
case Nil => "пустой список"
case _ => "что-то другое"
describe(5) // пять describe(true) // правда describe("hello") // привет!
describe(Nil) // пустой список describe(List(1,2,3)) // что-то другое
Патерны-переменные
Паттернпеременная соответствует любому объекту точно так же, как под
становочный паттерн, но в отличие от него Scala привязывает переменную к объекту. Затем с помощью этой переменной можно в дальнейшем воз
действовать на объект. Например, в листинге 13.6 показано сопоставление с образцом, имеющее специальный вариант для нуля и общий вариант для всех остальных значений. В общем варианте используется паттернпере
менная, и поэтому у него есть имя для переменной независимо от того, что это на самом деле.
13 .2 . Разновидности паттернов 273
Листинг 13.6. Сопоставление с образцом с использование паттерна-переменной expr match case 0 => "нуль"
case somethingElse => s"не нуль $somethingElse"
Переменная или константа?
У паттерновконстант могут быть символические имена. Вы уже это виде
ли, когда в качестве образца использовался
Nil
. А вот похожий пример, где в сопоставлении с образцом задействуются константы
E
(2,718 28...) и
Pi
(3,141 59...):
scala> import math.{E, Pi}
import math.{E, Pi}
scala> E match case Pi => s"математический казус? Pi = $Pi"
case _ => "OK"
val res0: String = OK
Как и ожидалось, значение
E
не равно значению
Pi
, поэтому вариант «мате
матический казус» не выбирается.
А как компилятор Scala распознает, что
Pi
— это константа, импортированная из scala.math
, а не переменная, обозначающая само значение селектора?
Во избежание путаницы в Scala действует простое лексическое правило: обычное имя, начинающееся с буквы в нижнем регистре, считается перемен
ной паттерна, а все другие ссылки считаются константами. Чтобы заметить разницу, создайте для pi псевдоним с первой буквой, указанной в нижнем регистре, и попробуйте в работе следующий код:
scala> val pi = math.Pi pi: Double = 3.141592653589793
scala> E match case pi => s"математический казус? Pi = $pi"
val res1: String = математический казус? Pi = 2.718281828459045
Здесь компилятор даже не позволит вам добавить вариант по умолчанию.
Поскольку pi
— паттернпеременная, то будет соответствовать всем вводи
мым данным, поэтому до следующих вариантов дело просто не дойдет:
scala> E match case pi => s"математический казус? Pi = $pi"
274 Глава 13 • Сопоставление с образцом case _ => "OK"
val res2: String = математический казус? Pi = 2.718281828459045 3 | case _ => "OK"
| ˆ
| Unreachable case
Но при необходимости для паттернаконстанты можно задействовать имя, начинающееся с буквы в нижнем регистре; для этого придется восполь
зоваться одним из двух приемов. Если константа является полем какого
нибудь объекта, то перед ней можно поставить префиксклассификатор.
Например, pi
— паттернпеременная, а this.pi или obj.pi
— константы, несмотря на то что их имена начинаются с букв в нижнем регистре. Если это не сработает (поскольку, скажем, pi
— локальная переменная), то как вариант можно будет заключить имя переменной в обратные кавычки.
Например,
`pi`
будет опять восприниматься как константа, а не как пере
менная:
scala> E match case `pi` => s"математический казус? Pi = $pi"
case _ => "OK"
res4: String = OK
Как вы, наверное, заметили, использование для идентификаторов в Scala синтаксиса с обратными кавычками во избежание в коде необычных обсто
ятельств преследует две цели. Здесь показано, что этот синтаксис может применяться для рассмотрения идентификатора с именем, начинающимся с буквы в нижнем регистре, в качестве константы при сопоставлении с об
разцом. Ранее, в разделе 6.10, было показано, что этот синтаксис может использоваться также для трактовки ключевого слова в качестве обычного идентификатора. Например, в выражении writingThread.`yield`()
слово yield трактуется как идентификатор, а не ключевое слово.
Паттерны-конструкторы
Реальная эффективность сопоставления с образцом проявляется имен
но в конструкторах. Паттернконструктор выглядит как
BinOp("+",
e,
Num(0))
. Он состоит из имени (
BinOp
), после которого в круглых скобках стоят несколько образцов:
"+"
, e
и
Num(0)
. При условии, что имя обознача
ет case
класс, такой паттерн показывает следующее: сначала проверяется принадлежность элемента к названному case
классу, а затем соответствие
13 .2 . Разновидности паттернов 275
параметров конструктора объекта предоставленным дополнительным пат
тернам.
Эти дополнительные паттерны означают, что в паттернах Scala поддержи
ваются глубкие сопоставления (deep matches). Такой паттерн проверяет не только предоставленный объект верхнего уровня, но и его содержимое на соответствие следующим паттернам. Дополнительные паттерны сами по себе могут быть паттернамиконструкторами, поэтому их можно использовать для проверки объекта произвольной глубины. Например, паттерн, показан
ный в листинге 13.7, проверяет, что объект верхнего уровня относится к типу
BinOp
, третьим параметром его конструктора является число
Num и значение поля этого числа —
0
. Весь паттерн умещается в одну строку кода, хотя вы
полняет проверку на глубину в три уровня.
Листинг 13.7. Сопоставление с образцом с использованием паттерна-конструктора expr match case BinOp("+", e, Num(0)) => "глубокое соответствие"
case _ => ""
Паттерны-последовательности case
классы можно сопоставлять с такими типами последовательностей, как
List или
Array
. Однако теперь в паттерне вы можете указать любое ко
личество элементов, пользуясь тем же синтаксисом. В листинге 13.8 показан шаблон для проверки трехэлементного списка, начинающегося с нуля.
Листинг 13.8. Паттерн-последовательность фиксированной длины xs match case List(0, _, _) => "соответствие найдено"
case _ => ""
Если нужно сопоставить с последовательностью, не указывая ее длину, то в качестве последнего элемента паттернапоследовательности можно указать образец
_*
. Он имеет весьма забавный вид и соответствует любому количеству элементов внутри последовательности, включая ноль элементов.
В листинге 13.9 показан пример, соответствующий любому списку, который начинается с нуля, независимо от длины этого списка.
Листинг 13.9. Паттерн-последовательность произвольной длины xs match case List(0, _, _) => " соответствие найдено "
case _ => ""
276 Глава 13 • Сопоставление с образцом
Паттерны-кортежи
Можно выполнять и сопоставление с кортежами. Паттерн вида
(a,
b,
c)
соответствует произвольному трехэлементному кортежу. Пример показан в листинге 13.10.
Если загрузить показанный в листинге 13.10 метод tupleDemo в интерпре
татор и передать ему кортеж из трех элементов, то получится следующая картина.
Листинг 13.10. Сопоставление с образцом с использованием паттерна-кортежа def tupleDemo(obj: Any) =
obj match case (a, b, c) => s"matched $a$b$c"
case _ => ""
tupleDemo(("a ", 3, "-tuple")) // соответствует a 3-tuple
Типизированные паттерны
Типизированный паттерн (typed pattern) можно использовать в качестве удобного заменителя для проверок типов и приведения типов. Пример по
казан в листинге 13.11.
Листинг 13.11. Сопоставление с образцом с использованием типизированных паттернов def generalSize(x: Any) =
x match case s: String => s.length case m: Map[_, _] => m.size case _ => -1
А вот несколько примеров использования generalSize в интерпретаторе
Scala:
generalSize("abc") // 3
generalSize(Map(1 –> 'a', 2 –> 'b')) // 2
generalSize(math.Pi) // -1
Метод generalSize возвращает размер или длину объектов различных типов.
Типом его аргумента является
Any
, поэтому им может быть любое значение.
Если в качестве типа аргумента выступает
String
, то метод возвращает длину строки. Образец s:
String является типизированным паттерном и соответ
13 .2 . Разновидности паттернов 277
ствует каждому (ненулевому) экземпляру класса
String
. Затем на эту строку ссылается паттернпеременная s
Заметьте: даже притом что s
и x
ссылаются на одно и то же значение, типом x
является
Any
, а типом s
является
String
. Поэтому в альтернативном выра
жении, соответствующем паттерну, можно воспользоваться кодом s.length
, но нельзя — кодом x.length
, поскольку в типе
Any отсутствует член length
Эквивалентный, но более многословный способ достичь такого же результата сопоставления с типизированным образцом — использовать проверку типа с его последующим приведением. В Scala для этого применяется не такой синтаксис, как в Java. К примеру, чтобы проверить, относится ли выражение expr к типу
String
, используется такой код:
expr.isInstanceOf[String]
Для приведения того же выражения к типу
String используется код expr.asInstanceOf[String]
Применяя проверку и приведение типа, можно переписать первый вариант предыдущего match
выражения, получив код, показанный в листинге 13.12.
Листинг 13.12. Использование isInstanceOf и asInstanceOf (плохой стиль)
if x.isInstanceOf[String] then val s = x.asInstanceOf[String]
s.length else ...
Операторы isInstanceOf и asInstanceOf считаются предопределенными методами класса
Any
, получающими параметр типа в квадратных скобках.
Фактически x.asInstanceOf[String]
— частный случай вызова метода с явно заданным параметром типа
String
Как вы уже заметили, написание проверок и приведений типов в Scala страдает излишним многословием. Сделано это намеренно, поскольку по
добная практика не приветствуется. Как правило, лучше воспользоваться сопоставлением с типизированным образцом. В частности, подобный подход будет оправдан, если нужно выполнить две операции: проверку типа и его приведение, так как обе они будут сведены к единственному сопоставлению.
Второй вариант match
выражения в листинге 13.11 содержит типизи
рованный паттерн m:
Map[_,
_]
. Он соответствует любому значению, явля ющемуся отображением какихлибо произвольных типов ключа
278 Глава 13 • Сопоставление с образцом и значения, и позволяет m
ссылаться на это значение. Поэтому m.size имеет правильный тип и возвращает размер отображения. Знаки подчеркивания в типизированном паттерне
1
подобны таким же знакам в подстановочных паттернах. Вместо них можно указывать переменные типа с символами в нижнем регистре.
Приписывание типов
Приведения по своей сути небезопасны. Например, даже если у ком
пилятора достаточно информации, чтобы определить, что приведение из
Int в
String не сработает во время выполнения, оно все равно ком
пилируется (и завершается сбоем во время выполнения):
3.asInstanceOf[String]
// java.lang.ClassCastException: java.lang.Integer
// не может быть приведен к java.lang.String
Безопасной альтернативой приведения является приписывание ти
пов: размещение двоеточия и типа после переменной или выражения.
Приписывание типов безопасно, потому что любое неправильное приписывание, например приписывание
Int к типу
String
, приведет к ошибке компилятора, а не к исключению во время выполнения:
scala> 3: String // ': String' — приписывание типов
1 |3: String
|ˆ
|Found: (3 : Int)
|Required: String
Приписывание типа будет компилироваться только в двух случаях.
Вопервых, вы можете использовать его для расширения типа до од
ного из его супертипов. Например:
scala> Var("x"): Expr // Expr — супертип Var val res0: Expr = Var(x)
Вовторых, вы можете использовать его для неявного преобразования одного типа в другой, например для неявного преобразования
Int в
Long
:
scala> 3: Long val res1: Long = 3 1
В типизированном паттерне m:
Map[_,
_],
часть "Map[_,
_]"
называется паттерном типа.
13 .2 . Разновидности паттернов 279
Затирание типов
А можно ли также проверять на отображение с конкретными типами элемен
тов? Это пригодилось бы, скажем, для проверки того, является ли заданное значение отображением типа
Int на тип
Int
. Попробуем:
scala> def isIntIntMap(x: Any) =
x match case m: Map[Int, Int] => true case _ => false def isIntIntMap(x: Any): Boolean
3 | case m: Map[Int, Int] => true
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| the type test for Map[Int, Int] cannot be
| checked at runtime
В Scala точно так же, как и в Java, используется модель затирания обобщен
ных типов. Это значит, в ходе выполнения программы никакая информация об аргументах типов не сохраняется. Следовательно, способов определить в ходе выполнения программы, создавался ли заданный
Map
объект с двумя
Int
аргументами, а не с аргументами других типов, не существует. Система может лишь определить, что значение является отображением (
Map
) неких произвольных параметров типа. Убедиться в таком поведении можно, при
менив isIntIntMap к различным экземплярам класса
Map
:
isIntIntMap(Map(1 –> 1)) // true isIntIntMap(Map("abc" –> "abc")) // true
Первое применение возвращает true
, что выглядит вполне корректно, но второе тоже возвращает true
, и это может оказаться сюрпризом. Чтобы оповестить вас о возможном непонятном поведении программы в ходе ее выполнения, компилятор выдает предупреждение о том, что не контролирует это поведение, похожее на показанные ранее.
Единственное исключение из правила затирания — массивы, поскольку в Java, а также в Scala они обрабатываются особым образом. Тип элемента массива сохраняется вместе со значением массива, поэтому к нему можно применить сопоставление с образцом. Пример выглядит так:
def isStringArray(x: Any) =
x match case a: Array[String] => "yes"
case _ => "no"
isStringArray(Array("abc")) // да isStringArray(Array(1, 2, 3)) // нет
280 Глава 13 • Сопоставление с образцом
Привязка переменной
Кроме использования отдельно взятого паттернапеременной, можно так
же добавить переменную к любому другому паттерну. Нужно указать имя переменной, знак «собачки» (
@
), а затем паттерн. Это даст вам паттерн с привязанной переменной, то есть паттерн для выполнения обычного со
поставления с образцом с возможностью в случае совпадения присвоить переменной соответствующий объект, как и при использовании обычного паттернапеременной.
В качестве примера в листинге 13.13 показано сопоставление с образцом — поиск операции получения абсолютного значения, применяемой в строке дважды. Такое выражение можно упростить, однократно получив абсолют
ное значение.
Листинг 13.13. Паттерн с привязкой переменной (посредством использования знака @)
expr match case UnOp("abs", e @ UnOp("abs", _)) => e case _ =>
Пример, показанный в данном листинге, включает паттерн с привязкой переменной, где в качестве переменной выступает e
, а в качестве паттерна —
UnOp("abs",
_)
. Если будет найдено соответствие всему паттерну, то часть, которая соответствует
UnOp("abs",
_)
, станет доступна как значение пере
менной e
. Результатом варианта будет просто e
, поскольку e
имеет значение, равное expr
, но с меньшим на единицу количеством операций получения абсолютного значения.
13 .3 . Ограждение образца
Иногда синтаксическое сопоставление с образцом является недостаточно точным. Предположим, перед вами стоит задача сформулировать правило упрощения, заменяющее выражение сложения с двумя одинаковыми операн
дами, такое как e
+
e
, умножением на два, например e
*
2
. На языке деревьев
Expr выражение вида
BinOp("+", Var("x"), Var("x"))
этим правилом будет превращено в
BinOp("*", Var("x"), Num(2))
13 .4 . Наложение паттернов 281
Правило можно попробовать выразить следующим образом:
scala> def simplifyAdd(e: Expr) =
e match case BinOp("+", x, x) => BinOp("*", x, Num(2))
case _ => e
3 | case BinOp("+", x, x) => BinOp("*", x, Num(2))
| ˆ
| duplicate pattern variable: x
Попытка будет неудачной, поскольку в Scala паттерны должны быть линей-
ными: паттернпеременная может появляться в образце только один раз.
Но, как показано в листинге 13.14, соответствие можно переформулировать с помощью ограничителя паттернов (pattern guard).
Листинг 13.14. Сопоставление с образцом с применением ограждения паттернов def simplifyAdd(e: Expr) =
e match case BinOp("+", x, y) if x == y =>
BinOp("*", x, Num(2))
case _ => e
Ограждение паттерна указывается после образца и начинается с ключевого слова if
. В качестве ограждения может использоваться произвольное бу
лево выражение, которое обычно ссылается на переменные в образце. При наличии ограждения паттернов соответствие считается найденным, только если ограждение вычисляется в true
. Таким образом, первый вариант по
казанного ранее кода соответствует только бинарным операциям, имеющим два одинаковых операнда.
А вот как выглядят некоторые другие огражденные паттерны:
// соответствует только положительным целым числам case n: Int if 0 < n => ...
// соответствует только строкам, начинающимся с буквы 'a'
case s: String if s(0) == 'a' => ...
13 .4 . Наложение паттернов
Паттерны применяются в порядке их указания. Версия метода simplify
, по
казанная в листинге 13.15, представляет собой пример, в котором порядок следования вариантов имеет значение.