Сопоставление с образцом
Сопоставление с образцом — это «разбор по форме»: вы пишете, как выглядят данные, и 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. Это делает функции ясными, а компилятор помогает не забыть ни одного случая.