Миран Липовача - Изучай Haskell во имя добра!
ghci> fromIntegral (length [1,2,3,4]) + 3.2
7.2
Несколько заключительных слов о классах типов
Поскольку класс типа определяет абстрактный интерфейс, один и тот же тип данных может иметь экземпляры для различных классов, а для одного и того же класса могут быть определены экземпляры различных типов. Например, тип Char имеет экземпляры для многих классов, два из которых – Eq и Ord, поскольку мы можем сравнивать символы на равенство и располагать их в алфавитном порядке.
Иногда для типа данных должен быть определён экземпляр некоторого класса для того, чтобы имелась возможность определить для него экземпляр другого класса. Например, для определения экземпляра класса Ord необходимо предварительно иметь экземпляр класса Eq. Другими словами, наличие экземпляра класса Eq является предварительным (необходимым) условием для определения экземпляра класса Ord. Если поразмыслить, это вполне логично: раз уж допускается расположение неких значений в определённом порядке, то должна быть предусмотрена и возможность проверить их на равенство.
3
Синтаксис функций
Сопоставление с образцом
В этой главе будет рассказано о некоторых весьма полезных синтаксических конструкциях языка 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
Это жалоба на то, что наши образцы не покрывают всех возможных случаев (недоопределены) – и, воистину, так оно и есть! Когда мы определяем функцию, мы должны всегда включать образец, который можно сопоставить с любым входным значением, для того чтобы наша программа не закрывалась с сообщением об ошибке, если функция получит какие-то непредвиденные входные данные.
Сопоставление с парами
Сопоставление с образцом может быть использовано и для кортежей. Что если мы хотим создать функцию, которая принимает два двумерных вектора (представленных в форме пары) и складывает их? Чтобы сложить два вектора, нужно сложить их соответствующие координаты. Вот как мы написали бы такую функцию, если б не знали о сопоставлении с образцом:
addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
addVectors a b = (fst a + fst b, snd a + snd b)
Это, конечно, сработает, но есть способ лучше. Давайте исправим функцию, чтобы она использовала сопоставление с образцом:
addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
Так гораздо лучше. Теперь ясно, что параметры функции являются кортежами; к тому же компонентам кортежа сразу даны имена – это повышает читабельность. Заметьте, что мы сразу написали образец, соответствующий любым значениям. Тип функции addVectors в обоих случаях совпадает, так что мы гарантированно получим на входе две пары:
ghci> :t addVectors
addVectors :: (Double, Double) -> (Double, Double) -> (Double, Double)
Функции fst и snd извлекают компоненты пары. Но как быть с тройками? Увы, стандартных функций для этой цели не существует, однако мы можем создать свои:
first :: (a, b, c) –> a
first (x, _, _) = x
second :: (a, b, c) –> b
second (_, y, _) = y
third :: (a, b, c) –> c
third (_, _, z) = z
Символ _ имеет то же значение, что и в генераторах списков. Он означает, что нам не интересно значение на этом месте, так что мы просто пишем _.
Сопоставление со списками и генераторы списков
В генераторах списков тоже можно использовать сопоставление с образцом, например:
ghci> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]
ghci> [a+b | (a,b) <– xs]
[4,7,6,8,11,4]
Если сопоставление с образцом закончится неудачей для одного элемента списка, просто произойдёт переход к следующему элементу.
Списки сами по себе (то есть заданные прямо в тексте образца списковые литералы) могут быть использованы при сопоставлении с образцом. Вы можете проводить сравнение с пустым списком или с любым образцом, который включает оператор : и пустой список. Так как выражение [1,2,3] – это просто упрощённая запись выражения 1:2:3:[], можно использовать [1,2,3] как образец.
Образец вида (x:xs) связывает «голову» списка с x, а оставшуюся часть – с xs, даже если в списке всего один элемент; в этом случае xs – пустой список.
ПРИМЕЧАНИЕ. Образец (x:xs) используется очень часто, особенно с рекурсивными функциями. Образцы, в определении которых присутствует :, могут быть использованы только для списков длиной не менее единицы.
Если вы, скажем, хотите связать первые три элемента с переменными, а оставшиеся элементы списка – с другой переменной, то можете использовать что-то наподобие (x:y:z:zs). Образец сработает только для списков, содержащих не менее трёх элементов.