Как уже было сказано, функциональная программа состоит из деклараций функций. Haskell является языком со строгой статической типизацией данных, поэтому в нем декларация функции состоит из описания типа функции и описания самой функции. Запись функций несколько отличается от общепринятых математических обозначений. Например, целочисленной функции
inc :: Int -> Int inc x = x + 1
Здесь первая строка объявляет, что inc
— это одноместная функция,
отображающая множество целых чисел Int
в него же, а вторая строка
содержит определение этого отображения. Такая декларация аналогична
декларации функций в императивных языках; ср. с определением той же функции на языке C:
int inc(int x) { return x + 1; }
Очень важно помнить, что символ =
обозначает определение функции,
а не операцию присваивания (ее в Haskell'е просто нет). В частности, строки
n :: Int n = 1определяют не переменную (их в Haskell'е тоже нет!), а нульместную функцию
n
,
которая возвращает целое значение 1.
Отсюда вытекает два важных следствия:
1. Определенную один раз функцию переопределить в той же программе нельзя (как и в императивных языках). Иными словами, в языке Haskell отсутствуют побочные эффекты; для получения нового значения мы вынуждены всякий раз создавать новую функцию. (Строго говоря, вычисление функций все же может иметь один побочный эффект — зацикливание.)
2. Расположение деклараций функций в программе не имеет никакого значения. Вычисление значения функции производится только в тот момент, когда оно действительно потребовалось (такой порядок вычислений называется ленивым или отложенным). Например, мы можем безболезненно написать:
div0 :: Int div0 = 1 / 0
Эта декларация функции не вызовет никаких проблем, пока какая-нибудь функция в программе
не попытается вычислить значение функции div0
; в этот момент, естественно,
возникнет фатальная ошибка.
Определим функцию bot
так:
bot = bot
Очевидно, что при вычислении функции bot
произойдет зацикливание программы.
Поскольку каждая функция в Haskell'е должна иметь значение, подобным функциям приписано
абстрактное значение _|_
, читается «дно» (bottom). Все функции,
в процессе вычисления которых произошла фатальная ошибка, также возвращают
значение _|_
(например, приведенная только что функция div0
).
В функциональном программировании функция f
называется строгой,
если значение f bot
равно _|_
. Иными словами, строгая функция,
примененная к зацикливающейся функции, также должна зациклиться. В большинстве языков
программирования все функции являются строгими; Haskell в этом смысле является исключением.
Пусть функция const1
определена так:
const1 x = 1
Тогда const1 bot
вернет значение 1. Здесь опять-таки срабатывает
ленивая модель вычислений: поскольку значение const1
не зависит от значения
ее аргумента, этот аргумент не вычисляется.
Коль скоро в Haskell'е зацикливание и возникновение фатальной ошибки семантически
эквивалентны, нетрудно догадаться, что const1 div0
также вернет значение 1.
Из приведенного определения функции inc
видно, что аргументы функций не
заключаются в круглые скобки, в отличие от привычной математической нотации. Такая запись
называется каррированной (curried, вот и обещанное упоминание фамилии Хаскелла Карри).
Все каррированные функции являются одноместными, т. е. функциями одного аргумента. При этом считается, что n-местная функция
В качестве примера приведем функцию, вычисляющую сумму двух целых чисел:
add :: Int -> Int -> Int add x y = x + y
Строгая запись этой декларации согласно приведенному выше правилу должна была бы иметь такой вид:
add :: Int -> (Int -> Int) (add x) y = x + y
Однако, мы можем опустить скобки в обеих строках благодаря принятому в языке Haskell правилу: описания типов имеют правую ассоциативность, а описания функций — левую ассоциативность.
Теперь мы можем с помощью функции add
написать определение новой функции,
эквивалентной функции inc
:
inc' :: Int -> Int inc' x = add 1 xили еще короче:
inc' = add 1
Отметим, что Haskell допускает использование некаррированных функций вида
add (x, y) = x + yно такая функция по-прежнему будет иметь только один аргумент — пару чисел
(x, y)
, а ее типом будет (Int, Int) -> Int
.
Haskell содержит две встроенные функции curry
и uncurry
,
которые соответственно выполняют «каррирование»
и «декаррирование» подобных функций, например:
add :: (Int, Int) -> Int add (x, y) = x + y inc x = curry add x 1 sub :: Int -> Int -> Int sub x y = x - y dec x = uncurry sub (x, 1)
(Писать определения типов для функций inc
и dec
здесь
необязательно, поскольку они однозначно определяются компилятором по типам
остальных функций).
Алонзо Чёрч ввел систему обозначений, не требующую присваивать функциям имена, которая известна как λ-нотация. В этой системе λx[e] обозначает функцию от переменной x, которая каждому значению x сопоставляет значение выражения e. λ-нотация естественно распространяется и на функции с несколькими аргументами. Например, следующим функциям в обычных обозначениях
Из этих примеров видно, что λ-нотация в точности соответствует соглашению о каррированных функциях. И, действительно, компилятор преобразует все функции во внутреннее λ-представление. Например, функции
inc x = x + 1 add x y = x + yявляются сокращенной записью внутреннего представления
inc = \x -> x + 1 add = \x y -> x + y
(Здесь обратная косая черта имитирует символ λ.)
Использование λ-нотации в большинстве случаев только затрудняет чтение программы. Однако, мы увидим далее, что у нее есть одно практическое применение: определение локальных анонимных функций внутри других функций.