ghci> zipWith' (+) [4,2,5,6] [2,6,2,3]
[6,8,7,9]
ghci> zipWith' max [6,3,2,1] [7,3,1,5]
[7,3,2,5]
ghci> zipWith' (++) ["шелдон ", "леонард "] ["купер", "хофстадтер"]
["шелдон купер","леонард хофстадтер"]
ghci> zipWith' (*) (replicate 5 2) [1..]
[2,4,6,8,10]
ghci> zipWith' (zipWith' (*)) [[1,2,3],[3,5,6],[2,3,4]] [[3,2,2],[3,4,5],[5,4,3]] [[3,4,6],[9,20,30],[10,12,12]]
Как видите, одна-единственная функция высшего порядка может применяться самыми разными способами.
Реализация функции flip
Теперь реализуем ещё одну функцию из стандартной библиотеки, flip
. Функция flip
принимает функцию и возвращает функцию. Единственное отличие результирующей функции от исходной – первые два параметра переставлены местами. Мы можем реализовать flip
следующим образом:
flip' :: (a –> b –> c) –> (b –> a –> c)
flip' f = g
where g x y = f y x
Читая декларацию типа, мы видим, что функция принимает на вход функцию с параметрами типов a
и b
и возвращает функцию с параметрами b
и a
. Так как все функции на самом деле каррированы, вторая пара скобок не нужна, поскольку символ –>
правоассоциативен. Тип (a –> b –> c) –> (b –> a –> c)
– то же самое, что и тип (a –> b –> c) –> (b –> (a –> c))
, а он, в свою очередь, представляет то же самое, что и тип (a –> b –> c) –> b –> a –> c
. Мы записали, что g x y = f y x
. Если это верно, то верно и следующее: f y x = g x y
. Держите это в уме – мы можем реализовать функцию ещё проще.
flip' :: (a –> b –> c) –> b –> a –> c
flip' f y x = f x y
Здесь мы воспользовались тем, что функции каррированы. Когда мы вызываем функцию flip' f
без параметров y
и x
, то получаем функцию, которая принимает два параметра, но переставляет их при вызове. Даже несмотря на то, что такие «перевёрнутые» функции обычно передаются в другие функции, мы можем воспользоваться преимуществами каррирования при создании ФВП, если подумаем наперёд и запишем, каков будет конечный результат при вызове полностью определённых функций.
ghci> zip [1,2,3,4,5,6] "привет"
[(1,'п'),(2,'р'),(3,'и'),(4,'в'),(5,'е'),(6,'т')]
ghci> flip' zip [1,2,3,4,5] "привет"
[('п',1),('р',2),('и',3),('в',4),('е',5),('т',6)]
ghci> zipWith div [2,2..] [10,8,6,4,2]
[0,0,0,0,1]
ghci> zipWith (flip' div) [2,2..] [10,8,6,4,2]
[5,4,3,2,1]
Если применить функцию flip'
к zip
, то мы получим функцию, похожую на zip
, за исключением того что элементы первого списка будут оказываться вторыми элементами пар результирующего списка, и наоборот. Функция flip' div
делит свой второй параметр на первый, так что если мы передадим ей числа 2
и 10
, то результат будет такой же, что и в случае div 10 2
.
Инструментарий функционального программиста
Как функциональные программисты мы редко будем обрабатывать одно значение. Обычно нам хочется сразу взять набор чисел, букв или значений каких-либо иных типов, а затем преобразовать всё это множество для получения результата. В данном разделе будет рассмотрен ряд полезных функций, которые позволяют нам работать с множествами значений.
Функция map
Функция map
берёт функцию и список и применяет функцию к каждому элементу списка, формируя новый список. Давайте изучим сигнатуру этой функции и посмотрим, как она определена.
map :: (a –> b) –> [a] –> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
Сигнатура функции говорит нам, что функция map
принимает на вход функцию, которая вычисляет значение типа b
по параметру типа a
, список элементов типа a
и возвращает список элементов типа b
. Интересно, что глядя на сигнатуру функции вы уже можете сказать, что она делает. Функция map
– одна из самых универсальных ФВП, и она может использоваться миллионом разных способов. Рассмотрим её в действии:
ghci> map (+3) [1,5,3,1,6]
[4,8,6,4,9]
ghci> map (++ "!") ["БУХ", "БАХ", "ПАФ"]
["БУХ!","БАХ!","ПАФ!"]
ghci> map (replicate 3) [3..6]
[[3,3,3],[4,4,4],[5,5,5],[6,6,6]]
ghci> map (map (^2)) [[1,2],[3,4,5,6],[7,8]]
[[1,4],[9,16,25,36],[49,64]]