Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
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, представляет собой пример, в котором порядок следования вариантов имеет значение. |