Ответ на вопрос
Кратко и по существу — отличия семантики сопоставления с образцом в Haskell, Rust и Scala: возможности и ограничения.
1) Общее поведение, которое важно помнить
- Во всех трёх — «первый подходящий» образец (first-match wins): сверху вниз выбирается первая пригодная ветка.
- Поддерживаются вложенные образцы, подстановочный (_) и as‑связывания (x@pat), а также guard'ы (доп. условия).
- Различаются правила о (а) проверке исчерпываемости, (б) когда образец «рефутируем» (refutable) vs «ирефутируем» (irrefutable), (в) связывании/передаче владения и (г) расширяемости образцов (view/extractor/GADT и т. п.).
2) Haskell
- Стратегия и строгость:
- Язык ленивый; но сопоставление в case и в заголовках функций заставляет вычисляться scrutinee до нужной степени (т.е. конструктор‑распознавание — «жёсткое»).
- let‑bindings ПО УМОЛЧАНИЮ иррефутируемы: `let (x,y) = ...` не немедленно проверяет соответствие (используется ленивость). Для заставления ленивости/отсрочки есть явный оператор иррефутируемости `~pat`.
- Провеки компилятора:
- GHC даёт предупреждения о не исчерпывающих и о лишних/недостижимых образцах; с GADTs/паттерн‑синонимами проверка может быть менее точной.
- Возможности образцов:
- rich: конструкторы данных (алгебрические типы), as‑patterns (`x@pat`), guards, view‑patterns, pattern synonyms, GADT‑распознавание, записи.
- Нативной концепции «extractor» как в Scala нет, но view‑patterns и pattern‑synonyms дают похожие возможности.
- Ограничения:
- Нельзя иметь «побочных эффектов» прямо в сопоставлении (чисто функционально), но можно делать вычисления в guard'ах.
- Сложная семантика в сочетании с ленивостью (let vs case vs function) требует осторожности.
Пример (важно: let иррефутируем, case — рефутируем):
- `let (x,y) = undefined` — ошибки не будет пока не обратиться к x/y.
- `case undefined of (x,y) -> ...` — немедленная ошибка.
3) Rust
- Стратегия и строгость:
- Чёткая, строгая семантика: сопоставление выполняется немедленно, match/if let/while let работают с текущим значением.
- `match` обязателен быть исчерпывающим; компилятор требует покрытие всех вариантов (или `_`), и проверяет недостижимые ветки.
- Refutable vs irrefutable:
- `match` и `if let`/`while let` — для рефутируемых образцов.
- `let pat = expr;` требует иррефутируемого образца; если образец может не сработать, используют `if let` или `match`.
- Взаимодействие с владением/заимствованием:
- Образцы управляют перемещением/заимствованием: при сопоставлении значение может быть перемещено, или можно писать `ref`, `ref mut`, `&pat` для заимствований, и `mut` для мутабельных связываний.
- Возможности образцов:
- OR‑паттерны (`A | B`), диапазоны для литералов (`0..=255`), срезы (`[a, b, ..rest]`), привязка `x @ pat`, guards, структурные паттерны для enum/struct/tuple, ref/mut, макросы/паттерн‑матчинг в const contexts.
- Ограничения:
- Нет произвольных view‑patterns (но можно реализовать через методы/функции и match).
- Семантика тесно связана с системой владения/заимствования — ошибки могут возникать из‑за move/borrow.
- Паттерны не дают (в общем) произвольной типовой рефинментации, как GADT в Haskell, хотя enum/trait system покрывает большинство задач.
- Примеры:
- `match opt { Some(x) => ..., None => ... }` — обязательно покрытие.
- `let Some(x) = opt;` — не допускается (рефутируемый паттерн в let), используйте `if let`.
4) Scala (2.x и Scala 3 — близки по семантике)
- Стратегия и строгость:
- Жёсткий, немедленный match; первая подходящая ветка выбирается.
- Непокрытый match ведёт к выбросу `MatchError` в runtime (компилятор может предупреждать/проверять для sealed типов).
- Возможности образцов:
- Constructor patterns для case‑классов, extractors через `unapply/unapplySeq` (очень мощно — произвольная логика извлечения), типовые паттерны (`x: T`), последовательности (`seq @ _*`), OR (`p1 | p2`), guards, as‑patterns (`x @ pat`), подстановочный `_`.
- Extractors дают аналог view‑patterns: любой объект с `unapply` может определить, как сопоставляться.
- Ограничения:
- Типовая информация может стираться (JVM type erasure) — сравнение по типам для параметризованных типов ограничено (нельзя надёжно матчить на `List[Int]` vs `List[String]`).
- Exhaustiveness проверяется лучше для sealed hierarchies, но может быть неточной из‑за unapply и дженериков/erasure.
- val‑pattern/val‑bindings рефутируемы: `val Some(x) = opt` бросит `MatchError`, т.е. нет «иррефутируемых let‑паттернов» как в Rust.
- Примеры:
- `case class Cons(h,t)` -> `list match { case Cons(x,xs) => ...; case Nil => ... }`
- Extractor: `object Even { def unapply(n: Int): Boolean = n%2==0 }` — `x match { case Even() => ... }`.
5) Сравнение по важным аспектам (сжатый свод)
- Exhaustiveness checking:
- Haskell: компилятор даёт предупреждения; GADT/символы усложняют.
- Rust: строгая, обязательная для `match`.
- Scala: проверяется для sealed hierarchies; runtime MatchError возможен.
- Refutable vs irrefutable in bindings:
- Haskell: `let` — иррефутируемый по умолчанию (ленивость), `case`/функции — рефутируемые; есть `~` для явной иррефутируемости.
- Rust: `let` требует иррефутируемости; рефутируемые — в `match`/`if let`.
- Scala: `val` паттерны рефутируемы — бросают `MatchError` если не совпадают.
- Type refinements / GADTs / extractors:
- Haskell: сильная типовая система, GADTs дают статическую рефинментацию типа при матчинге.
- Rust: enums + pattern matching даёт статическую проверку вариантов; GADT‑аналогов нет в том же виде, но enum + pattern + типовая система покрывают большинство нужд.
- Scala: мощные extractors (`unapply`) и типовые паттерны, но JVM‑erasure ограничивает generics; Scala 3 улучшает проверку и expressivity.
- Ownership / Mutability:
- Haskell: чисто функционально, без мутаций.
- Rust: паттерны влияют на move/borrow; можно писать `ref`/`mut`.
- Scala: обычные ссылки/объекты JVM, mutability зависит от типов; паттерны не управляют владением.
- Расширяемость:
- Haskell: pattern synonyms, view patterns, GADTs.
- Rust: нет встроенных view patterns, но мощные паттерны языка + макросы.
- Scala: unapply/extractor — очень гибко (можно реализовать любую логику распознавания).
6) Практические рекомендации
- Если нужна строгая, безопасная проверка покрытия и управление владением — Rust (и его compile‑time exhaustiveness/reachability).
- Если нужна мощная типовая рефинментация на уровне типов (GADTs), чисто функциональная семантика и декларативные pattern‑synonyms — Haskell.
- Если нужна гибкость извлечения/преобразования (view‑like) и интеграция с ООП/JVM, — Scala (extractors/unapply), но следите за erasure и runtime MatchError.
Если нужно — могу привести 2–3 коротких код‑примера (по одному для каждого языка), иллюстрирующих ключевые различия.
Еще