3. Функции Haskell (продолжение)

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

Во многих случаях алгоритм вычисления функции зависит от ее аргументов. В этом случае мы можем определить функцию путем сопоставления с образцом (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

Помимо лучшей читабельности, такая запись дает подсказку компилятору, что значение аргумента можно не вычислять (т. е. опять воспользоваться ленивыми вычислениями).

Рекурсия и if-then-else

В императивных языках программирования мы можем для задания рекуррентных алгоритмов выбирать между циклами и рекурсией. В ФП такого выбора нет — отсутствие переменных означает отсутствие циклов.

Для демонстрации простейшей рекурсивной функции в учебниках традиционно используют факториал. Отступим от этой традиции и приведем функцию, вычисляющую числа Фибоначчи:

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-выражения

Еще один способ записи функций, семантически эквивалентный сопоставлению с образцом, состоит в использовании ключевого слова 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