Во многих случаях алгоритм вычисления функции зависит от ее аргументов. В этом случае мы можем определить функцию путем сопоставления с образцом (pattern matching), например:
isZero :: Int -> Bool isZero 0 = True isZero x = False
Эта функция отображает целое число в булевское значение (тип Bool
в языке
Haskell имеет значения True
и False
). При ее вычислении производится
последовательное сравнение аргумента с заданными образцами (у нас это 0
и
x
) и, если сопоставление с образцом прошло успешно, вычисляется значение,
соответствующее этому образцу. Если ни один из образцов не удалось сопоставить с аргументом,
то происходит фатальная ошибка «неудача сопоставления с образцом».
При задании функции подобным образом важен порядок записи строк сопоставления. Если мы
в нашем примере переставим строки декларации функции, то получим функцию, которая всегда
возвращает значение False
:
nevermore :: Int -> Bool nevermore x = False nevermore 0 = True
Еще одно важное замечание. В функции isZero
мы использовали произвольно
выбранное обозначение x
для аргумента, значение которого нас не интересует
(сопоставления с первым образцом не произошло, поэтому x
отличен от нуля, а
остальное уже не важно). В подобных случаях рекомендуется использовать специальное обозначение
_
(любой аргумент):
isZero 0 = True isZero _ = False
Помимо лучшей читабельности, такая запись дает подсказку компилятору, что значение аргумента можно не вычислять (т. е. опять воспользоваться ленивыми вычислениями).
В императивных языках программирования мы можем для задания рекуррентных алгоритмов выбирать между циклами и рекурсией. В ФП такого выбора нет — отсутствие переменных означает отсутствие циклов.
Для демонстрации простейшей рекурсивной функции в учебниках традиционно используют факториал. Отступим от этой традиции и приведем функцию, вычисляющую числа Фибоначчи:
fibb :: Int -> Int fibb 0 = 1 fibb 1 = 1 fibb n = fibb (n-2) + fibb (n-1)
(Круглые скобки здесь не имеют отношения к декаррированию функций, они просто задают порядок вычислений).
Эта функция прекрасно работает на натуральных числах, но попытка вызвать
fibb (-1)
вызывает фатальный вылет интерпретатора Hugs
по переполнению стека. К счастью, Haskell содержит вполне традиционную условную конструкцию
if-then-else
, что позволяет нам написать данную функцию корректно:
fibb' :: Int -> Int fibb' 0 = 1 fibb' 1 = 1 fibb' n = if n < 0 then error "Negative argument" else fibb' (n-2) + fibb' (n-1)
Этот код требует двух пояснений. Во-первых, мы использовали встроенную функцию
error
, которая порождает фатальную ошибку с заданным текстовым сообщением.
Во-вторых, последняя строка написана с отступом от левого края и не случайно.
В Haskell-программах не принято (хотя и допустимо) использовать разделитель операторов
«точка с запятой». Вместо этого используются следующие правила форматирования
программ:
Помимо условной конструкции if-then-else
, мы можем использовать более компактную
ее запись с помощью булевских охранников (guards). Охранники удобны в тех случаях,
когда нам нужно записать несколько альтернативных вариантов. В качестве примера рассмотрим
функцию, возвращающую знак числа:
sign :: Int -> Int sign x | x < 0 = -1 | x == 0 = 0 | x > 0 = 1
Как видно из этого примера, каждый охранник состоит из вертикальной черты и булевского условия.
Последним в списке охранников может стоять специальный охранник otherwise
(в остальных случаях):
fibb'' :: Int -> Int fibb'' 0 = 1 fibb'' 1 = 1 fibb'' n | n < 0 = error "Negative argument" | otherwise = fibb'' (n-2) + fibb'' (n-1)
Как и в случае сопоставления с образцом, список охранников просматривается последовательно, поэтому их порядок можеть иметь существенное значение для правильности вычисления функции.
Вопрос для самопроверки: что сделает следующая функция при попытке вычислить fibb''' (-1)
?
fibb''' :: Int -> Int fibb''' 0 = 1 fibb''' 1 = 1 fibb''' n | n > 1 = fibb''' (n-2) + fibb''' (n-1)
Ответ: произойдет фатальная ошибка «неудача сопоставления с образцом».
Еще один способ записи функций, семантически эквивалентный сопоставлению с образцом,
состоит в использовании ключевого слова case
. Функция fibb
с
использованием case-выражений записывается так:
fibb n = case n of 0 -> 1 1 -> 1 _ -> if n > 1 then fibb (n-2) + fibb (n-1) else error "Negative argument"
В действительности, все сопоставления с образцом и все условные конструкции преобразуются
компилятором в case-выражения, которые считаются примитивными операциями. Например, условная
конструкция if a then b else c
будет преобразована в
case a of True -> b False -> c
Во многих случаях для вычисления значения функции бывает нужно вычислить некоторые дополнительные значения, которые имеют смысл только в процессе данного вычисления. В императивных языках для этого используются блоки и локальные переменные.
Haskell поддерживает два аналогичных механизма, позволяющих нам определять локальные функции внутри определения какой-либо функции. Имена таких функций невидимы вне определения головной функции и поэтому могут использоваться в других частях программы, не вызывая конфликтов.
Первый механизм использует ключевое слово where
.
В качестве примера найдем решение квадратного уравнения
ax2+bx+c=0
(для простоты примем, что a отлично от нуля).
roots :: (Float, Float, Float) -> (Float, Float) roots (a,b,c) = if d < 0 then error "No roots" else (x1, x2) where x1 = (-b + sd) / (2 * a) x2 = (-b - sd) / (2 * a) d = b * b - 4 * a * c sd = sqrt d