ПРИМЕЧАНИЕ. Удостоверьтесь в том, что вы поняли, как работает каррирование и частичное применение функций, поскольку эти понятия очень важны.
Немного о высоких материях
Функции могут принимать функции в качестве параметров и возвращать функции в качестве значений. Чтобы проиллюстрировать это, мы собираемся создать функцию, которая принимает функцию, а затем дважды применяет её к чему-нибудь!
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
, но не наоборот.
ПРИМЕЧАНИЕ. Запомните: когда вы создаёте функции, особенно высших порядков, и не уверены, каким должен быть тип, вы можете попробовать опустить объявление типа, а затем проверить, какой тип выведет язык Haskell, используя команду :t
в GHCi.
Устройство данной функции очень похоже на обычную функцию zip
. Базовые случаи одинаковы. Единственный дополнительный аргумент – соединяющая функция, но он не влияет на базовые случаи; мы просто используем для него маску подстановки _
. Тело функции в последнем образце также очень похоже на функцию zip
– разница в том, что она не создаёт пару (x, y)
, а возвращает f x y
. Одна функция высшего порядка может использоваться для решения множества задач, если она достаточно общая. Покажем на небольшом примере, что умеет наша функция zipWith'
: