Pattern Matching
От: x-code  
Дата: 28.12.20 17:31
Оценка:
Провожу сравнительный анализ паттерн матчинга в таких языках как 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. Просто удобство? Или есть какие-то случаи, когда такие аргументы дают нечто качественно другое, чего нельзя получить обращением к полям аргумента?
Отредактировано 28.12.2020 17:32 x-code . Предыдущая версия .
Re: Pattern Matching
От: Буравчик Россия  
Дата: 28.12.20 18:00
Оценка: 4 (1) +2
Здравствуйте, x-code, Вы писали:

XC>И вот тут вопрос: а что дают такие переменные? Ведь все данные уже есть в аргументе оператора switch или match. Просто удобство? Или есть какие-то случаи, когда такие аргументы дают нечто качественно другое, чего нельзя получить обращением к полям аргумента?


1. У аргумента может не быть полей вовсе (например, неименованный кортеж). Лучше написать (name, age), чем tuple[0], tuple[1]

2. Паттерн-матчинг может идти не только по полям, но и по структуре. Пример из хаскелл — сопоставление условия (x:xs) для списка [1,2,3]. Это выглядит лучше, чем обращение к методам исходного объекта — lst.head(), lst.tail().

3. Можно давать различные имена переменных для каждого варианта (ветки) матчинга, иногда может быть удобно.
Best regards, Буравчик
Re: Pattern Matching
От: varenikAA  
Дата: 29.12.20 06:36
Оценка: 4 (1)
Здравствуйте, x-code, Вы писали:

XC>И вот тут вопрос: а что дают такие переменные? Ведь все данные уже есть в аргументе оператора switch или match. Просто удобство? Или есть какие-то случаи, когда такие аргументы дают нечто качественно другое, чего нельзя получить обращением к полям аргумента?


Так в этом и смысл сопоставления с шаблоном, что он сразу делает нужное преобразование, вместо кодера.
и да лишние символы коде типа let это явно оверхед замусоривающий код.

и да, матч это выражение которое можно посчитать, а свитч оператор, который определяет условие выполнения той или иной ветки.
в сишарпе ужасно поступили — там switch в одном случае выражение в другом оператор. нарушен принцип простоты. теперь кодер должен думать о различиях.
☭ ✊ В мире нет ничего, кроме движущейся материи.
Re: Pattern Matching
От: maxkar  
Дата: 01.01.21 09:12
Оценка: 4 (1)
Здравствуйте, 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 — это как-то странно.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.