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

В этой главе будет рассказано о некоторых весьма полезных синтаксических конструкциях языка Haskell, и начнём мы с сопоставления с образцом. Идея заключается в указании определённых шаблонов – образцов, которым должны соответствовать некоторые данные. Во время выполнения программы данные проверяются на соответствие образцу (сопоставляются). Если они подходят под образец, то будут разобраны в соответствии с ним.

Когда вы определяете функцию, её определение можно разбить на несколько частей (клозов), по одной части для каждого образца. Это позволяет создать очень стройный код, простой и легко читаемый. Вы можете задавать образцы для любого типа данных – чисел, символов, списков, кортежей и т. д. Давайте создадим простую функцию, которая проверяет, является ли её параметр числом семь.

lucky :: Int -> String

lucky 7 = "СЧАСТЛИВОЕ ЧИСЛО 7!"

lucky x = "Прости, друг, повезёт в другой раз!"

Когда вы вызываете функцию lucky, производится проверка параметра на совпадение с заданными образцами в том порядке, в каком они были заданы. Когда проверка даст положительный результат, используется соответствующее тело функции. Единственный случай, когда число, переданное функции, удовлетворяет первому образцу, – когда оно равно семи. В противном случае проводится проверка на совпадение со следующим образцом. Следующий образец может быть успешно сопоставлен с любым числом; также он привязывает переданное число к переменной x.

Если в образце вместо реального значения (например, 7) пишут идентификатор, начинающийся со строчной буквы (например, x, y или myNumber), то этот образец будет сопоставлен любому переданному значению. Обратиться к сопоставленному значению в теле функции можно будет посредством введённого идентификатора.

Эта функция может быть реализована с использованием ключевого слова if. Ну а если нам потребуется написать функцию, которая называет цифры от 1 до 5 и выводит "Это число не в пределах от 1 до 5" для других чисел? Без сопоставления с образцом нам бы пришлось создать очень запутанное дерево условных выражений if – then – else. А вот что получится, если использовать сопоставление:

sayMe :: Int -> String

sayMe 1 = "Один!"

sayMe 2 = "Два!"

sayMe 3 = "Три!"

sayMe 4 = "Четыре!"

sayMe 5 = "Пять!"

sayMe x = "Это число не в пределах от 1 до 5"

Заметьте, что если бы мы переместили последнюю строку определения функции (образец в которой соответствует любому вводу) вверх, то функция всегда выводила бы "Это число не в пределах от 1 до 5", потому что невозможно было бы пройти дальше и провести проверку на совпадение с другими образцами.

Помните реализованную нами функцию факториала? Мы определили факториал числа n как произведение чисел [1..n]. Мы можем определить данную функцию рекурсивно, точно так же, как факториал определяется в математике. Начнём с того, что объявим факториал нуля равным единице.

Затем определим факториал любого положительного числа как данное число, умноженное на факториал предыдущего числа. Вот как это будет выглядеть в терминах языка Haskell.

factorial :: Integer -> Integer

factorial 0 = 1

factorial n = n * factorial (n – 1)

Мы в первый раз задали функцию рекурсивно. Рекурсия очень важна в языке Haskell, и подробнее она будет рассмотрена позже.

Сопоставление с образцом может завершиться неудачей, если мы зададим функцию следующим образом:

charName :: Char –> String

charName 'а' = "Артём"

charName 'б' = "Борис"

charName 'в' = "Виктор"

а затем попытаемся вызвать её с параметром, которого не ожидали. Произойдёт следующее:

ghci> charName 'а'

"Артём"

ghci> charName 'в'

"Виктор"

ghci> charName 'м'

"*** Exception: Non-exhaustive patterns in function charName

Это жалоба на то, что наши образцы не покрывают всех возможных случаев (недоопределены) – и, воистину, так оно и есть! Когда мы определяем функцию, мы должны всегда включать образец, который можно сопоставить с любым входным значением, для того чтобы наша программа не закрывалась с сообщением об ошибке, если функция получит какие-то непредвиденные входные данные.