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

  • Листинг 13.9.

  • Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста


    Скачать 6.24 Mb.
    НазваниеОдерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
    Дата27.04.2023
    Размер6.24 Mb.
    Формат файлаpdf
    Имя файлаScala. Профессиональное программирование 2022.pdf
    ТипДокументы
    #1094967
    страница30 из 64
    1   ...   26   27   28   29   30   31   32   33   ...   64
    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, представляет собой пример, в котором порядок следования вариантов имеет значение.

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


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