Изучай Haskell во имя добра! - Миран Липовача
Шрифт:
Интервал:
Закладка:
Посмотрим на эту простую функцию:
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.
Сечения
Инфиксные функции могут быть частично применены при помощи так называемых сечений. Для построения сечения инфиксной функции достаточно поместить её в круглые скобки и предоставить параметр только с одной стороны. Это создаст функцию, которая принимает один параметр и применяет его к стороне с пропущенным операндом. Вот донельзя простой пример:
divideByTen :: (Floating a) => a –> a
divideByTen = (/10)
Вызов, скажем, divideByTen 200 эквивалентен вызову 200 / 10, равно как и (/10) 200:
ghci> divideByTen 200
20.0
ghci> 200 / 10
20.0
ghci> (/10) 200
20.0
А вот функция, которая проверяет, находится ли переданный символ в верхнем регистре:
isUpperAlphanum :: Char –> Bool
isUpperAlphanum = (`elem` ['А'..'Я'])
Единственная особенность при использовании сечений – применение знака «минус». По определению сечений, (–4) – это функция, которая вычитает четыре из переданного числа. В то же время для удобства (–4) означает «минус четыре». Если вы хотите создать функцию, которая вычитает четыре из своего аргумента, выполняйте частичное применение таким образом: (subtract 4).
Печать функций
До сих пор мы давали частично применённым функциям имена, после чего добавляли недостающие параметры, чтобы всё-таки посмотреть на результаты. Однако мы ни разу не попробовали напечатать сами функции. Попробуем? Что произойдёт, если мы попробуем выполнить multThree 3 4 в GHCi вместо привязки к имени с помощью ключевого слова let либо передачи другой функции?
ghci> multThree 3 4
<interactive>:1:0:
No instance for (Show (a –> a))
arising from a use of `print' at <interactive>:1:0–12
Possible fix: add an instance declaration for (Show (a –> a))
In the expression: print it
In a 'do' expression: print it
GHCi сообщает нам, что выражение порождает функцию типа a –> a, но он не знает, как вывести её на экран. Функции не имеют экземпляра класса Show, так что мы не можем получить точное строковое представление функций. Когда мы вводим, скажем, 1 + 1 в терминале GHCi, он сначала вычисляет результат (2), а затем вызывает функцию show для 2, чтобы получить текстовое представление этого числа. Текстовое представление 2 – это строка "2", которая и выводится на экран.
ПРИМЕЧАНИЕ. Удостоверьтесь в том, что вы поняли, как работает каррирование и частичное применение функций, поскольку эти понятия очень важны.
Немного о высоких материях
Функции могут принимать функции в качестве параметров и возвращать функции в качестве значений. Чтобы проиллюстрировать это, мы собираемся создать функцию, которая принимает функцию, а затем дважды применяет её к чему-нибудь!
applyTwice :: (a –> a) –> a –> a
applyTwice f x = f (f x)
Прежде всего, обратите внимание на объявление типа. Раньше мы не нуждались в скобках, потому что символ –> обладает правой ассоциативностью. Однако здесь скобки обязательны. Они показывают, что первый параметр – это функция, которая принимает параметр некоторого типа и возвращает результат того же типа. Второй параметр имеет тот же тип, что и аргумент функции – как и возвращаемый результат. Мы можем прочитать данное объявление в каррированном стиле, но, чтобы избежать головной боли, просто скажем, что функция принимает два параметра и возвращает результат. Первый параметр – это функция (она имеет тип a –> a), второй параметр имеет тот же тип a. Заметьте, что совершенно неважно, какому типу будет соответствовать типовая переменная a – Int, String или вообще чему угодно – но при этом все значения должны быть одного типа.
ПРИМЕЧАНИЕ. Отныне мы будем говорить, что функция принимает несколько параметров, вопреки тому что в действительности каждая функция принимает только один параметр и возвращает частично применённую функцию. Для простоты будем говорить, что a –> a –> a принимает два параметра, хоть мы и знаем, что происходит «за кулисами».
Тело функции applyTwice достаточно простое. Мы используем параметр f как функцию, применяя её к параметру x (для этого разделяем их пробелом), после чего передаём результат снова в функцию f. Давайте поэкспериментируем с функцией:
ghci> applyTwice (+3) 10
16
ghci> applyTwice (++ " ХА-ХА") "ЭЙ"
"ЭЙ ХА-ХА ХА-ХА"
ghci> applyTwice ("ХА-ХА " ++) "ЭЙ"
"ХА-ХА ХА-ХА ЭЙ"
ghci> applyTwice (multThree 2 2) 9
144
ghci> applyTwice (3:) [1]
[3,3,1]
Красота и полезность частичного применения очевидны. Если наша функция требует передать ей функцию одного аргумента, мы можем частично применить функцию-параметр таким образом, чтобы оставался неопределённым всего один параметр, и затем передать её нашей функции. Например, функция + принимает два параметра; с помощью сечений мы можем частично применить её так, чтобы остался только один.
Реализация функции zipWith
Теперь попробуем применить ФВП для реализации очень полезной функции из стандартной библиотеки. Она называется zipWith. Эта функция принимает функцию и два списка, а затем соединяет списки, применяя переданную функцию для соответствующих элементов. Вот как мы её реализуем:
zipWith' :: (a –> b –> c) –> [a] –> [b] –> [c]
zipWith' _ [] _ = []
zipWith' _ _ [] = []
zipWith' f (x:xs) (y:ys) = f x y : zipWith' f xs ys
Посмотрите на объявление типа. Первый параметр – это функция, которая принимает два значения и возвращает одно. Параметры этой функции не обязательно должны быть одинакового типа, но могут. Второй и третий параметры – списки. Результат тоже является списком. Первым идёт список элементов типа a, потому что функция сцепления принимает значение типа a в качестве первого параметра. Второй должен быть списком из элементов типа b, потому что второй параметр у связывающей функции имеет тип b. Результат – список элементов типа c. Если объявление функции говорит, что она принимает функцию типа a –> b –> c как параметр, это означает, что она также примет и функцию a –> a –> a, но не наоборот.