Каррированные функции
Каждая функция в языке Haskell официально может иметь только один параметр. Но мы определяли и использовали функции, которые принимали несколько параметров. Как же такое может быть? Да, это хитрый трюк! Все функции, которые принимали несколько параметров, были каррированы. Функция называется каррированной, если она всегда принимает только один параметр вместо нескольких. Если потом её вызвать, передав этот параметр, то результатом вызова будет новая функция, принимающая уже следующий параметр.
Легче всего объяснить на примере. Возьмём нашего старого друга – функцию max. Если помните, она принимает два параметра и возвращает максимальный из них. Если сделать вызов max 4 5, то вначале будет создана функция, которая принимает один параметр и возвращает 4 или поданный на вход параметр – смотря что больше. Затем значение 5 передаётся в эту новую функцию, и мы получаем желаемый результат. В итоге оказывается, что следующие два вызова эквивалентны:
ghci> max 4 5
5
ghci> (max 4) 5
5
Чтобы понять, как это работает, давайте посмотрим на тип функции max:
ghci> :t max
max :: (Ord a) => a –> a –> a
То же самое можно записать иначе:
max :: (Ord a) => a –> (a –> a)
Прочитать запись можно так: функция max принимает параметр типа a и возвращает (–>) функцию, которая принимает параметр типа a и возвращает значение типа a. Вот почему возвращаемый функцией тип и параметры функции просто разделяются стрелками.
Ну и чем это выгодно для нас? Проще говоря, если мы вызываем функцию и передаём ей не все параметры, то в результате получаем новую функцию, а именно – результат частичного применения исходной функции. Новая функция принимает столько параметров, сколько мы не использовали при вызове оригинальной функции. Частичное применение (или, если угодно, вызов функции не со всеми параметрами) – это изящный способ создания новых функций «на лету»: мы можем передать их другой функции или передать им ещё какие-нибудь параметры.
Посмотрим на эту простую функцию:
multThree :: Int -> Int -> Int -> Int
multThree x y z = x * y * z
Что происходит, если мы вызываем multThree 3 5 9 или ((multThree 3) 5) 9? Сначала значение 3 применяется к multThree, так как они разделены пробелом. Это создаёт функцию, которая принимает один параметр и возвращает новую функцию, умножающую на 3. Затем значение 5 применяется к новой функции, что даёт функцию, которая примет параметр и умножит его уже на 15. Значение 9 применяется к этой функции, и получается результат 135. Вы можете думать о функциях как о маленьких фабриках, которые берут какие-то материалы и что-то производят. Пользуясь такой аналогией, мы даём фабрике multThree число 3, и, вместо того чтобы выдать число, она возвращает нам фабрику немного поменьше. Эта новая фабрика получает число 5 и тоже выдаёт фабрику. Третья фабрика при получении числа 9 производит, наконец, результат — число 135. Вспомним, что тип этой функции может быть записан так:
multThree :: Int -> (Int -> (Int -> Int))
Перед символом –> пишется тип параметра функции; после записывается тип значения, которое функция вернёт. Таким образом, наша функция принимает параметр типа Int и возвращает функцию типа Int -> (Int –> Int). Аналогичным образом эта новая функция принимает параметр типа Int и возвращает функцию типа Int -> Int. Наконец, функция принимает параметр типа Int и возвращает значение того же типа Int.
Рассмотрим пример создания новой функции путём вызова функции с недостаточным числом параметров:
ghci> let multTwoWithNine = multThree 9
ghci> multTwoWithNine 2 3
54
В этом примере выражение multThree 9 возвращает функцию, принимающую два параметра. Мы называем эту функцию multTwoWithNine. Если при её вызове предоставить оба необходимых параметра, то она перемножит их между собой, а затем умножит произведение на 9.
Вызывая функции не со всеми параметрами, мы создаём новые функции «на лету». Допустим, нужно создать функцию, которая принимает число и сравнивает его с константой 100. Можно сделать это так:
compareWithHundred :: Int -> Ordering
compareWithHundred x = compare 100 x
Если мы вызовем функцию с 99, она вернёт значение GT. Довольно просто. Обратите внимание, что параметр x находится с правой стороны в обеих частях определения. Теперь подумаем, что вернёт выражение compare 100. Этот вызов вернёт функцию, которая принимает параметр и сравнивает его с константой 100. Ага-а! Не этого ли мы хотели? Можно переписать функцию следующим образом:
compareWithHundred :: Int -> Ordering
compareWithHundred = compare 100
Объявление типа не изменилось, так как выражение compare 100 возвращает функцию. Функция compare имеет тип (Ord a) => a –> (a –> Ordering). Когда мы применим её к 100, то получим функцию, принимающую целое число и возвращающую значение типа Ordering.