Провожу сравнительный анализ паттерн матчинга в таких языках как Rust, Scala и Swift. И вот что получается.
1. По сравнению с классическим switch иp С/С++ аргументом оператора может быть любой объект любого типа, а не только целое число.
Это вполне логичная унифкация, но это лишь расширение switch и не делает сопоставление с образцом чем-то особенным.
2. Возможны дополнительные условия в операторе when, который интегрирован с оператором switch/match в том смысле, что если условие when ложно, то поиск образца будет продолжен
3. Образцами, кроме полноценных объектов, могут быть и "частичные" (если можно так сказать) объекты, т.е. составные объекты, у которых заданы не все поля. Те поля, которые не важны для сравнения, отмечаются допустим символом подчеркивания _
4. Вместо подчеркивания можно прямо в образце объявлять переменные, которые принимают значение соответствующих полей аргумента при совпадении образца с аргументом.
Синтаксис может отличаться, вот в Swift это наиболее явно и наглядно
let personInfo = ("Tom", 22)
switch personInfo {
case (let name, 22):
print("Имя: \(name) и возраст: 22")
case ("Tom", let age):
print("Имя: Tom и возраст: \(age)")
case let (name, age):
print("Имя: \(name) и возраст: \(age)")
}
В Rust и Scala новые имена берутся "с потолка" (без var или let) и потому для меня это менее наглядно.
В некотором смысле, можно сказать, что в образце часть полей "для чтения" (по ним осуществляется сравнение), а другая часть — "для записи" (в них записываются поля из аргумента в случае совпадения образца с аргументом).
И вот тут вопрос: а что дают такие переменные? Ведь все данные уже есть в аргументе оператора switch или match. Просто удобство? Или есть какие-то случаи, когда такие аргументы дают нечто качественно другое, чего нельзя получить обращением к полям аргумента?
Здравствуйте, x-code, Вы писали:
XC>1. По сравнению с классическим switch иp С/С++ аргументом оператора может быть любой объект любого типа, а не только целое число.
XC>Это вполне логичная унифкация, но это лишь расширение switch и не делает сопоставление с образцом чем-то особенным.
В C switch имеет свой вид, потому что в некоторых ассемблерах есть инструкции табличного перехода. Например, в x86 (не помню с какой модели процессора) есть две инструкции перехода — по "непрерывному" диапазону, и по ключ-значение (с целочисленными ключами). А другие языки берут свое сопоставление с образцом скорее из ML-семейства языков. Там оно всегда было в общем виде. Чуть дальше я даже пример приведу, почему по-другому быть и не могло.
Я на Scala специализируюсь, поэтому если отдельно не указано, все примеры и описания относятся к ней.
XC>В некотором смысле, можно сказать, что в образце часть полей "для чтения" (по ним осуществляется сравнение), а другая часть — "для записи" (в них записываются поля из аргумента в случае совпадения образца с аргументом).
Слишком грубо. Потому что в образце все поля — это другие образцы. В одном сase можно делать анализ нескольких уровней структуры сразу. Некоторые образцы являются константами для сравнения. Некоторые — группами захвата (т.е. переменными). Или это может быть сложный образец, включающий другие и обрабатываемый рекурсивно.
XC>И вот тут вопрос: а что дают такие переменные? Ведь все данные уже есть в аргументе оператора switch или match. Просто удобство? Или есть какие-то случаи, когда такие аргументы дают нечто качественно другое, чего нельзя получить обращением к полям аргумента?
Вы пропустили основной случай, когда нужных полей в аргументе нет. Современный pattern matching в огромной степени изначально делался для разбора размеченных объединений (discriminated union).
abstract sealed class MyTry[+T]
final case class MyFailure(exception: Throwable) extends MyTry[Nothing]
final case class MySuccess[T](result: T) extends MyTry[T]
// somewhere else
def process(result: MyTry[Int]): Unit =
result match {
case MyFailure(e) => println("An error occured: " + e.getMessage)
case MySuccess(v) => println("Twice the result is " + (2 * v))
}
Можно было бы достать поля через приведение типа и доступ. Но это очень грязно получится и много кода. Плюс в прародителе (ML-ях) приведения типов в таком виде не было вообще. Как раз там сопоставление с образцом было вообще единственным способом получить хоть что-то из исходного значения:
type myTry 'a = MyFailure of exception | MySuccess of 'a
let process v =
match v with
MyFailure e -> ""
MySuccess v -> ""
Кроме того, в Scala в качестве образца может выступать любое значение с одним из трех методов (unapply, unapplySeq и третий я не помню, может быть вариант unapply возвращающий Boolean). Например,
val overDisp = "(.*) over (.*)".r
val underDisp = "(.*) under (.*)".r
def printDisposition(sentence: String): Unit =
sentence match {
case overDisp(a, b) => println(s"Over(${a}, ${b})" )
case underDisp("cat", _) =>
println("Illegal dispotion of cat, it should always be over an object")
case underDisp(a, b) => println(s"Under(${a}, ${b})" )
case _ => println("Unknown disposition")
}
printDisposition("cat over desk")
printDisposition("cat under desk")
Разбирать string можно, конечно. Но в виде "поля" там нужного точно ничего нет.
XC> В Rust и Scala новые имена берутся "с потолка" (без var или let) и потому для меня это менее наглядно.
Стандартный сценарий использования pattern matching — это именно захват значений в переменные. Необходимость сравнения со значением другой переменной — это большая редкость и экзотика. Поэтому шум в виде val просто не нужен (та же история с for comprehension, где val сначала позволялся а потом его сделали deprecated). Если очень нужно, есть и синтаксис для сравнения (а не захвата):
final class Team {
private val teamId = java.util.UUID.randomUUID().toString()
def classifyPlayer(playerTeam: Option[String]): String =
playerTeam match {
case None => "Spectator"
case Some(`teamId`) => "Friend"
case Some(_) => "Enemy"
}
}
Еще вроде бы есть конвенции на заглавную/строчную буквы переменной (заглавные вроде бы считаются константами по-умолчанию), но я не уверен.
Ну и еще два момента, которые делают val неудобным. Первый — это захват значений "внутреннего" образца (т.е. не самого нижнего уровня):
def extractGoodList(items: Seq[Seq[Int]]): Seq[Int] =
items match {
case Seq(_, a@Seq(_, 42, _*), _*) => a
case Seq(b@Seq(_, 84, _*)) => b
case _ => Seq.empty
}
Второй аргумент — val сам по себе — это тоже сопоставление с образцом. С левой части от символа "равно" стоит именно образец с группами захвата:
val (a, b) = (5, 7)
val Seq(c, d, e) = Seq("a", "b", "c")
Пихать val внутри val — это как-то странно.