Обратите внимание на рекурсивный вызов функции foldNat мы обходим всё дерево значения, заменяя
каждый конструктор. Определим знакомые функции через свёртку:
isZero :: Nat -> Bool
isZero =
foldNat True (const False)Посмотрим как вычисляется эта функция:
isZero Zero
=>
True
-- заменили конструктор Zero
isZero (Succ
(Succ (Succ Zero)))=>
const False
(const False (const False True))-- заменили и Zero и Succ
=>
False
Что интересно за счёт ленивых вычислений на самом деле во втором выражении произойдёт лишь одна
замена. Мы не обходим всё дерево, нам это и не нужно, а смотрим лишь на первый конструктор, если там
Succ
, то произойдёт замена на постоянную функцию, которая игнорирует свой второй аргумент и рекурсив-ного вызова функции свёртки не произойдёт, совсем как в исходном определении!
even, odd :: Nat -> Bool
even
=
foldNat Truenot
odd
=
foldNat False notЭти функции определяют чётность числа, сдесь мы пользуемся тем свойством, что not (not a) ==
a.Определим сложение и умножение:
add, mul :: Nat -> Nat -> Nat
add a
=
foldNat aSucc
mul a
=
foldNat Zero(add a)
Свёртка | 193
Maybe
Вспомним определение типа для результата частично определённых функций:
data Maybe
a = Nothing | Just aПерепишем словно это класс:
data Maybe
a b whereNothing ::
bJust
::
a -> bЭтот класс принимает два параметра, поскольку исходный тип Maybe
принимает один. Теперь несложнодогадаться как будет выглядеть функция свёртки, мы просто получим стандартную функцию maybe. Дадим
определение экземпляра функтора и монады через свёртку:
instance Functor Maybe where
fmap f =
maybe Nothing (Just . f)instance Monad Maybe where
return
= Just
ma >>=
mf=
maybe Nothing mf maСписки
Функция свёртки для списков это функция foldr. Выведем её из определения типа:
data
[a] = a : [a] | []Представим, что это класс:
class
[a] b wherecons
::
a -> b -> bnil
::
bТеперь получить определение для foldr совсем просто:
foldr ::
(a -> b -> b) -> b -> [a] -> bfoldr cons nil =
\x -> case x ofa:
as->
a ‘cons‘ foldr cons nil as[]
->
nilМы обходим дерево значения, заменяя конструкторы методами нашего воображаемого класса. Опреде-
лим несколько стандартных функций для списков через свёртку.
Первый элемент списка:
head ::
[a] -> ahead =
foldr const (error ”empty list”)Объединение списков:
(++
) :: [a] -> [a] -> [a]a ++
b = foldr (:) b aВ этой функции мы реконструируем заново первый список но в самом конце заменяем пустой список в
хвосте a на второй аргумент, так и получается объединение списков. Обратите внимание на эту особенность,
скорость выполнения операции (++
) зависит от длины первого списка. Поэтому между двумя выражениями((a ++
b) ++ c) ++ da ++
(b ++ (c ++ d))Нет разницы в итоговом результате, но есть огромная разница по скорости вычисления! Второй гораздо
быстрее. Убедитесь в этом! Реализуем объединение списка списков в один список:
concat ::
[[a]] -> [a]concat =
foldr (++) []194 | Глава 12: Структурная рекурсия
Через свёртку можно реализовать и функцию преобразования списков:
map ::
(a -> b) -> [a] -> [b]map f =
foldr ((:) . f) []Если смысл выражения ((:
) . f) не совсем понятен, давайте распишем его типы:f
(:
)a
------->
b
------->
([b] -> [b])
Напишем функцию фильтрации:
filter ::
(a -> Bool) -> [a] -> [a]filter p =
foldr (\a as -> foldBool (a:as) as (p a)) []Тут у нас целых две функции свёртки. Если значение предиката p истинно, то мы вернём все элементы
списка, а если ложно отбросим первый элемент. Через foldr можно даже определить функцию с хвостовой
рекурсией foldl. Но это не так просто. Всё же попробуем. Для этого вспомним определение:
foldl ::
(a -> b -> a) -> a -> [b] -> afoldl f s []
=
sfoldl f s (a:
as)=
foldl f (f s a) asНам нужно привести это определение к виду foldr, нам нужно выделить два метода воображаемого
класса списка cons и nil:
foldr ::
(a -> b -> b) -> b -> [a] -> bfoldr cons nil =
\x -> case x ofa:
as->
a ‘cons‘ foldr cons nil as[]
->
nilПеренесём два последних аргумента определения foldl в правую часть, воспользуемся лямбда-
функциями и case
-выражением:foldl ::
(a -> b -> a) -> [b] -> a -> afoldl f =
\x -> case x of[]
->
\s -> sa:
as->
\s -> foldl f as (f s a)Мы поменяли местами порядок следования аргументов (второго и третьего). Выделим тождественную
функцию в первом уравнении case
-выражения и функцию композиции во втором.foldl ::
(a -> b -> a) -> [b] -> a -> afoldl f =
\x -> case x of[]
->
ida:
as->
foldl f as . (flip f a)Теперь выделим функции cons и nil:
foldl ::
(a -> b -> a) -> [b] -> a -> afoldl f =
\x -> case x of[]
->
nila:
as->
a ‘cons‘ foldl f aswhere
nil=
idcons
=
\a b -> b . flip f a=
\a->
( . flip f a)Теперь запишем через foldr:
foldl ::
(a -> b -> a) -> a -> [b] -> afoldl f s xs =
foldr (\a -> ( . flip f a)) id xs s