Сопоставление с образцом

Сопоставление с образцом — это «разбор по форме»: вы пишете, как выглядят данные, и Haskell сам выбирает подходящую ветку.
Вместо лестницы из if вы перечисляете случаи: «если ноль — то так, если что угодно другое — то эдак». Компилятор ещё и предупредит, если случай забыт.

Pattern matching — одна из самых приятных черт Haskell. Функцию можно определить несколькими уравнениями, каждое — для своей «формы» аргумента:

describe :: Int -> String
describe 0 = "ноль"
describe 1 = "один"
describe _ = "много"

Haskell проверяет образцы сверху вниз и берёт первый подходящий. Символ _ — это «что угодно», подстановочный образец, который ловит все оставшиеся случаи.

Разбор структуры

Образцы умеют заглядывать внутрь данных. Список разбирается на «голову и хвост» через (x:xs):

firstOrZero :: [Int] -> Int
firstOrZero []     = 0       -- пустой список
firstOrZero (x:_)  = x       -- голова x, хвост неважен

sumList :: [Int] -> Int
sumList []     = 0
sumList (x:xs) = x + sumList xs

Здесь [] сопоставляется с пустым списком, а (x:xs) — с непустым, связывая x с первым элементом и xs с остатком. Это основа рекурсии по спискам.

[1,2,3]  как образец (x:xs):
  x  = 1        (голова)
  xs = [2,3]    (хвост)

[]       — отдельный образец для пустого списка

Кортежи и охранные выражения

Кортежи разбираются по позициям, а уточнить условие помогают guards (охранники) со знаком |:

fst3 :: (a, b, c) -> a
fst3 (x, _, _) = x

grade :: Int -> String
grade score
  | score >= 90 = "отлично"
  | score >= 60 = "норм"
  | otherwise   = "пересдача"

Guards читаются как «при таком условии — такой результат». otherwise — это просто True, ловушка для всех остальных случаев.

В Python начиная с 3.10 появился похожий match — отличная аналогия:

# Та же идея на Python: structural pattern matching
def describe(n):
    match n:
        case 0: return "ноль"
        case 1: return "один"
        case _: return "много"

print(describe(0), describe(1), describe(5))

# Разбор списка на голову и хвост
def sum_list(xs):
    match xs:
        case []:          return 0
        case [x, *rest]:  return x + sum_list(rest)

print(sum_list([1, 2, 3]))   # 6

Образцы делают невозможное невозможным

Сила сопоставления с образцом не только в краткости, но и в том, что оно работает рука об руку с системой типов. Когда вы разбираете значение собственного типа, компилятор знает все его конструкторы и может проверить, что вы покрыли каждый случай. Включив предупреждение о неполных образцах, вы получаете гарантию: ни один вариант данных не останется без обработки, а значит, не будет неприятного падения на «забытом» случае. Образцы вкладываются друг в друга: можно за один разбор достать и поле записи, и элемент вложенного списка, и компонент кортежа. Это превращает обработку данных в декларативное описание «вот как выглядят варианты и что с каждым делать» — куда надёжнее, чем лестница условий, где легко перепутать порядок или пропустить ветку. Чем точнее ваши типы, тем больше пользы приносит сопоставление с образцом.

Как это мыслить

Описывайте данные по их форме, а не проверяйте их вопросами. Не «является ли список пустым?», а «вот случай для пустого, вот для непустого». Такой код читается как таблица случаев и почти не оставляет места ошибкам.

Стоит упомянуть и про связку образцов с конструкцией case ... of, которая позволяет сопоставлять с образцом не только в определении функции, но и прямо внутри выражения. Это удобно, когда разбор нужен локально, в одной ветке вычисления. Семантика та же: перечисляете возможные формы значения и результат для каждой. Вместе с guards и подстановочным образцом _ это покрывает практически любую логику ветвления, делая её декларативной и проверяемой компилятором. Чем больше вы пишете на Haskell, тем реже тянетесь к вложенным if: сопоставление с образцом выражает выбор по форме данных яснее и безопаснее, а полнота разбора подстраховывается предупреждениями компилятора.

Частые ошибки

  • Забыть случай. Если не покрыть все формы, программа может упасть на неучтённом входе; включайте предупреждения компилятора.
  • Перепутать порядок. Образцы проверяются сверху вниз — общий _ должен идти последним.
  • Связать одно имя дважды. Нельзя написать (x, x), ожидая «равные элементы» — для этого нужен guard.

Best practices

  • Ставьте _ для частей, которые не используете — это явно и понятно.
  • Сначала базовые случаи ([], 0), потом рекурсивные.
  • Включайте флаг -Wincomplete-patterns, чтобы ловить забытые случаи.

Итог. Сопоставление с образцом разбирает данные по форме: списки через (x:xs), кортежи по позициям, тонкости — через guards. Это делает функции ясными, а компилятор помогает не забыть ни одного случая.

Проверьте себя
1. Что связывает образец (x:xs) при сопоставлении со списком [1,2,3]?
Ax = [1,2,3], xs = []
Bx = 1, xs = [2,3]
Cx = 3, xs = [1,2]
Dx = [1,2], xs = 3
2. Что такое otherwise в охранных выражениях (guards)?
AКлючевое слово для цикла
BСиноним True — ловит все оставшиеся случаи
CОператор сравнения
DСпособ вернуть ошибку