Написание фолдла с использованием фолдра
В реальном мире Haskell, глава 4. Функциональное программирование
Напишите фолд с фолд:
-- file: ch04/Fold.hs
myFoldl :: (a -> b -> a) -> a -> [b] -> a
myFoldl f z xs = foldr step id xs z
where step x g a = g (f a x)
Вышеприведенный код меня сильно смутил, и какой-то парень по имени dps переписал его с каким-то значимым именем, чтобы сделать его более понятным:
myFoldl stepL zeroL xs = (foldr stepR id xs) zeroL
where stepR lastL accR accInitL = accR (stepL accInitL lastL)
Один парень, Джеф Г, тогда проделал отличную работу, шаг за шагом предоставив пример и продемонстрировав основной механизм:
myFoldl (+) 0 [1, 2, 3]
= (foldR step id [1, 2, 3]) 0
= (step 1 (step 2 (step 3 id))) 0
= (step 1 (step 2 (\a3 -> id ((+) a3 3)))) 0
= (step 1 (\a2 -> (\a3 -> id ((+) a3 3)) ((+) a2 2))) 0
= (\a1 -> (\a2 -> (\a3 -> id ((+) a3 3)) ((+) a2 2)) ((+) a1 1)) 0
= (\a1 -> (\a2 -> (\a3 -> (+) a3 3) ((+) a2 2)) ((+) a1 1)) 0
= (\a1 -> (\a2 -> (+) ((+) a2 2) 3) ((+) a1 1)) 0
= (\a1 -> (+) ((+) ((+) a1 1) 2) 3) 0
= (+) ((+) ((+) 0 1) 2) 3
= ((0 + 1) + 2) + 3
Но я все еще не могу полностью понять это, вот мои вопросы:
- Для чего нужна функция id? Какова роль? Зачем нам это нужно здесь?
- В приведенном выше примере функция id является аккумулятором в лямбда-функции?
- Прототип foldr - это foldr:: (a -> b -> b) -> b -> [a] -> b , и первый параметр - это функция, для которой нужны два параметра, но в шаговой функции в реализации myFoldl используются 3 параметра Я совершенно сбит с толку!
Есть кто-нибудь, кто может мне помочь? Большое спасибо!
10 ответов
Некоторые объяснения в порядке!
Для чего нужна функция id? Какова роль? Зачем нам это нужно здесь?
id
это тождественная функция, id x = x
и используется как эквивалент нуля при построении цепочки функций с композицией функций, (.)
, Вы можете найти его в Прелюдии.
В приведенном выше примере функция id является аккумулятором в лямбда-функции?
Аккумулятор - это функция, которая создается с помощью повторного применения функции. Там нет явного лямбда, так как мы называем аккумулятор, step
, Вы можете написать это с лямбдой, если хотите:
foldl f a bs = foldr (\b g x -> g (f x b)) id bs a
Или как Грэм Хаттон написал бы:
5.1
foldl
операторТеперь давайте обобщим из
suml
пример и рассмотрим стандартный операторfoldl
который обрабатывает элементы списка в порядке слева направо, используя функциюf
объединить значения и значениеv
в качестве начального значения:foldl :: (β → α → β) → β → ([α] → β) foldl f v [ ] = v foldl f v (x : xs) = foldl f (f v x) xs
Используя этот оператор,
suml
может быть переопределено простоsuml = foldl (+) 0
, Многие другие функции могут быть определены простым способом, используяfoldl
, Например, стандартная функцияreverse
можно переопределить с помощьюfoldl
следующее:reverse :: [α] → [α] reverse = foldl (λxs x → x : xs) [ ]
Это определение более эффективно, чем наше первоначальное определение с использованием сгиба, потому что оно избегает использования неэффективного оператора добавления
(++)
для списков.Простое обобщение расчета в предыдущем разделе для функции
suml
показывает, как переопределить функциюfoldl
с точки зренияfold
:foldl f v xs = fold (λx g → (λa → g (f a x))) id xs v
В отличие от этого, невозможно переопределить
fold
с точки зренияfoldl
, благодаря тому факту, чтоfoldl
строг в хвосте своего аргумента списка, ноfold
не является. Существует ряд полезных "теорем двойственности", касающихсяfold
а такжеfoldl
а также некоторые руководящие принципы для определения того, какой оператор лучше всего подходит для конкретных приложений (Bird, 1998).
прототипом foldr является foldr:: (a -> b -> b) -> b -> [a] -> b
Программист Haskell сказал бы, что тип foldr
является (a -> b -> b) -> b -> [a] -> b
,
и первый параметр - это функция, которой нужны два параметра, но пошаговая функция в реализации myFoldl использует 3 параметра, я совершенно запутался
Это запутанно и волшебно! Мы разыгрываем хитрость и заменяем аккумулятор функцией, которая, в свою очередь, применяется к начальному значению для получения результата.
Грэм Хаттон объясняет хитрость foldl
в foldr
в приведенной выше статье. Мы начнем с записи рекурсивного определения foldl
:
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f v [] = v
foldl f v (x : xs) = foldl f (f v x) xs
И затем рефакторинг его через преобразование статического аргумента на f
:
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f v xs = g xs v
where
g [] v = v
g (x:xs) v = g xs (f v x)
Давайте теперь перепишем g
чтобы отпустить v
внутренности:
foldl f v xs = g xs v
where
g [] = \v -> v
g (x:xs) = \v -> g xs (f v x)
Что так же, как думать о g
как функция одного аргумента, которая возвращает функцию:
foldl f v xs = g xs v
where
g [] = id
g (x:xs) = \v -> g xs (f v x)
Теперь у нас есть g
, функция, которая рекурсивно просматривает список, применяет некоторую функцию f
, Конечным значением является функция тождества, и каждый шаг также приводит к функции.
Но у нас уже есть очень похожая рекурсивная функция в списках, foldr
!
2 оператор сгиба
fold
оператор берет свое начало в теории рекурсии (Kleene, 1952), в то время как использованиеfold
как центральная концепция языка программирования восходит к оператору редукции APL (Iverson, 1962), а позже к оператору вставки FP (Backus, 1978). В Хаскелеfold
Оператор для списков может быть определен следующим образом:fold :: (α → β → β) → β → ([α] → β) fold f v [ ] = v fold f v (x : xs) = f x (fold f v xs)
То есть, учитывая функцию
f
типаα → β → β
и значениеv
типаβ
, функцияfold f v
обрабатывает список типов[α]
дать значение типаβ
заменив нулевой конструктор[]
в конце списка по значениюv
и каждый минус конструктор(:)
в списке по функцииf
, Таким образом,fold
Оператор инкапсулирует простой шаблон рекурсии для обработки списков, в котором два конструктора для списков просто заменяются другими значениями и функциями. Ряд знакомых функций в списках имеет простое определение, используяfold
,
Это похоже на рекурсивную схему, очень похожую на нашу g
функция. Теперь хитрость: используя всю доступную магию под рукой (иначе говоря, Берд, Меертенс и Малкольм), мы применяем специальное правило, универсальное свойство сгиба, которое является эквивалентностью двух определений для функции. g
который обрабатывает списки, представленные как:
g [] = v g (x:xs) = f x (g xs)
если и только если
g = fold f v
Итак, универсальное свойство складок гласит:
g = foldr k v
где g
должно быть эквивалентно двум уравнениям, для некоторых k
а также v
:
g [] = v
g (x:xs) = k x (g xs)
Из наших более ранних сгибов мы знаем v == id
, Для второго уравнения, однако, нам нужно вычислить определение k
:
g (x:xs) = k x (g xs)
<=> g (x:xs) v = k x (g xs) v -- accumulator of functions
<=> g xs (f v x) = k x (g xs) v -- definition of foldl
<= g' (f v x) = k x g' v -- generalize (g xs) to g'
<=> k = \x g' -> (\a -> g' (f v x)) -- expand k. recursion captured in g'
Который, подставляя наши расчетные определения k
а также v
дает определение foldl как:
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f v xs =
foldr
(\x g -> (\a -> g (f v x)))
id
xs
v
Рекурсивный g
заменяется комбинатором Foldr, и аккумулятор становится функцией, построенной через цепочку композиций f
в каждом элементе списка в обратном порядке (чтобы мы свернули влево, а не вправо).
Это определенно несколько продвинуто, поэтому для глубокого понимания этого преобразования, универсального свойства сгибов, которое делает возможным преобразование, я рекомендую учебное пособие Хаттона, приведенное ниже.
Рекомендации
- Haskell Wiki: Foldl as foldr
- Учебник по универсальности и выразительности складки, Грэм Хаттон, Дж. Функциональное программирование 9 (4): 355–372, июль 1999.
- Малькольм Дж. Алгебраические типы данных и преобразование программ. Докторская диссертация, Университет Гронингена.
Рассмотрим тип foldr
:
foldr :: (b -> a -> a) -> a -> [b] -> a
В то время как тип step
это что-то вроде b -> (a -> a) -> a -> a
, Поскольку шаг переходит к foldr
можно сделать вывод, что в этом случае складка имеет вид (b -> (a -> a) -> (a -> a)) -> (a -> a) -> [b] -> (a -> a)
,
Не смущайтесь различными значениями a
в разных подписях; это просто переменная типа. Также имейте в виду, что стрелка функции является ассоциативной справа, поэтому a -> b -> c
это то же самое, что a -> (b -> c)
,
Итак, да, значение аккумулятора для foldr
является функцией типа a -> a
и начальное значение id
, Это имеет некоторый смысл, потому что id
это функция, которая ничего не делает - это та же самая причина, по которой вы начинаете с нуля в качестве начального значения при добавлении всех значений в списке.
Что касается step
принимая три аргумента, попробуйте переписать это так:
step :: b -> (a -> a) -> (a -> a)
step x g = \a -> g (f a x)
Это облегчает видеть, что происходит? Он принимает дополнительный параметр, потому что он возвращает функцию, и два способа ее написания эквивалентны. Обратите внимание также на дополнительный параметр после foldr
: (foldr step id xs) z
, Часть в скобках - это сама складка, которая возвращает функцию, которая затем применяется к z
,
Вот мое доказательство того, что foldl
может быть выражено в терминах foldr
, который я нахожу довольно простым, кроме названия спагетти step
функция вводит.
Предложение состоит в том, что foldl f z xs
эквивалентно
myfoldl f z xs = foldr step_f id xs z
where step_f x g a = g (f a x)
Первое, на что следует обратить внимание, это то, что правая часть первой строки фактически оценивается как
(foldr step_f id xs) z
поскольку foldr
принимает только три параметра. Это уже намекает на то, что foldr
будет вычислять не значение, а функцию карри, которая затем применяется к z
, Есть два случая, чтобы выяснить, является ли myfoldl
является foldl
:
Базовый случай: пустой список
myfoldl f z [] = foldr step_f id [] z (by definition of myfoldl) = id z (by definition of foldr) = z foldl f z [] = z (by definition of foldl)
Непустой список
myfoldl f z (x:xs) = foldr step_f id (x:xs) z (by definition of myfoldl) = step_f x (foldr step_f id xs) z (-> apply step_f) = (foldr step_f id xs) (f z x) (-> remove parentheses) = foldr step_f id xs (f z x) = myfoldl f (f z x) xs (definition of myfoldl) foldl f z (x:xs) = foldl f (f z x) xs
Поскольку в обоих случаях первая и последняя строки имеют одинаковую форму, их можно использовать для сворачивания списка до xs == []
, в этом случае 1. гарантирует тот же результат. Итак, по индукции, myfoldl == foldl
,
(быстро просмотрите мои ответы [1], [2], [3], [4], чтобы убедиться, что вы понимаете синтаксис Haskell, функции высшего порядка, каррирование, составление функций, оператор $, операторы инфикса / префикса, разделы и лямбды).)
Универсальное свойство сгиба
Складка - это просто кодификация определенных видов рекурсии. А свойство универсальности просто утверждает, что, если ваша рекурсия соответствует определенной форме, она может быть преобразована в складку в соответствии с некоторыми формальными правилами. И наоборот, каждая складка может быть преобразована в рекурсию такого рода. Еще раз, некоторые рекурсии могут быть переведены в сгибы, которые дают точно такой же ответ, а некоторые рекурсии не могут, и для этого существует точная процедура.
По сути, если ваша рекурсивная функция работает со списками, похожими на левые, вы можете преобразовать ее, чтобы сложить одну вправо, подставив f
а также v
за то, что на самом деле там.
g [] = v ⇒
g (x:xs) = f x (g xs) ⇒ g = foldr f v
Например:
sum [] = 0 {- recursion becomes fold -}
sum (x:xs) = x + sum xs ⇒ sum = foldr 0 (+)
Вот v = 0
а также sum (x:xs) = x + sum xs
эквивалентно sum (x:xs) = (+) x (sum xs)
, следовательно f = (+)
, Еще 2 примера
product [] = 1
product (x:xs) = x * product xs ⇒ product = foldr 1 (*)
length [] = 0
length (x:xs) = 1 + length xs ⇒ length = foldr (\_ a -> 1 + a) 0
Упражнение:
Воплощать в жизнь
map
,filter
,reverse
,concat
а такжеconcatMap
рекурсивно, как и вышеупомянутые функции на левой стороне.Преобразуйте эти 5 функций в сложение в соответствии с формулой выше, то есть подставляя
f
а такжеv
в формуле сгиба справа.
Foldl через Foldr
Как написать рекурсивную функцию, которая суммирует числа слева направо?
sum [] = 0 -- given `sum [1,2,3]` expands into `(1 + (2 + 3))`
sum (x:xs) = x + sum xs
Первая рекурсивная функция, которая приходит к выводу, полностью раскрывается еще до того, как начинает складываться, это не то, что нам нужно. Один из подходов заключается в создании рекурсивной функции с аккумулятором, которая немедленно складывает числа на каждом шаге (читайте о хвостовой рекурсии, чтобы узнать больше о стратегиях рекурсии):
suml :: [a] -> a
suml xs = suml' xs 0
where suml' [] n = n -- auxiliary function
suml' (x:xs) n = suml' xs (n+x)
Хорошо, остановись! Запустите этот код в GHCi и убедитесь, что вы понимаете, как он работает, а затем тщательно и вдумчиво продолжайте. suml
не может быть переопределено со сгибом, но suml'
может быть.
suml' [] = v -- equivalent: v n = n
suml' (x:xs) n = f x (suml' xs) n
suml' [] n = n
из определения функции, верно? А также v = suml' []
из универсальной формулы собственности. Вместе это дает v n = n
, функция, которая немедленно возвращает все, что она получает: v = id
, Давайте посчитаем f
:
suml' (x:xs) n = f x (suml' xs) n
-- expand suml' definition
suml' xs (n+x) = f x (suml' xs) n
-- replace `suml' xs` with `g`
g (n+x) = f x g n
Таким образом, suml' = foldr (\x g n -> g (n+x)) id
и поэтому, suml = foldr (\x g n -> g (n+x)) id xs 0
,
foldr (\x g n -> g (n + x)) id [1..10] 0 -- return 55
Теперь нам просто нужно обобщить, заменить +
переменной функцией:
foldl f a xs = foldr (\x g n -> g (n `f` x)) id xs a
foldl (-) 10 [1..5] -- returns -5
Заключение
Теперь прочитайте учебник Грэма Хаттона об универсальности и выразительности сгиба. Возьми ручку и бумагу, постарайся выяснить все, что он пишет, пока ты сам не получишь большинство складок. Не парьтесь, если вы чего-то не понимаете, вы всегда можете вернуться позже, но тоже не откладывайте на потом.
Нет никакой Королевской Дороги к Математике, ни даже через Haskell. Позволять
h z = (foldr step id xs) z where
step x g = \a -> g (f a x)
Что за хрень h z
? Предположим, что xs = [x0, x1, x2]
,
Примените определение Foldr:
h z = (step x0 (step x1 (step x2 id))) z
Примените определение шага:
= (\a0 -> (\a1 -> (\a2 -> id (f a2 x2)) (f a1 x1)) (f a0 x0)) z
Подставим в лямбда-функции:
= (\a1 -> (\a2 -> id (f a2 x2)) (f a1 x1)) (f z x0)
= (\a2 -> id (f a2 x2)) (f (f z x0) x1)
= id (f (f (f z x0) x1) x2)
Применить определение id:
= f (f (f z x0) x1) x2
Примените определение сгиба:
= foldl f z [x0, x1, x2]
Это Royal Road или что?
Прежде чем голосовать против, прочтите следующий абзац.
Я отправляю ответ для тех людей, которым этот подход может лучше соответствовать их образу мышления. Ответ, возможно, содержит избыточную информацию и мысли, но это то, что мне нужно для решения проблемы. Кроме того, поскольку это еще один ответ на тот же вопрос, очевидно, что он существенно пересекается с другими ответами, однако он рассказывает историю о том, как я мог понять эту концепцию.
На самом деле я начал записывать эти заметки как личную запись своих мыслей, пытаясь понять эту тему. Мне потребовался целый день, чтобы прикоснуться к его сути, если я действительно понял.
Мой долгий путь к пониманию этого простого упражнения
Легкая часть: что нам нужно определить?
Что происходит в следующем примере вызова
foldl f z [1,2,3,4]
можно визуализировать с помощью следующей диаграммы (которая есть в Википедии, но я впервые увидел ее в другом ответе):
_____results in a number
/
f f (f (f (f z 1) 2) 3) 4
/ \
f 4 f (f (f z 1) 2) 3
/ \
f 3 f (f z 1) 2
/ \
f 2 f z 1
/ \
z 1
(В качестве примечания, при использовании foldl
каждое приложение f
не выполняется, и выражения обрабатываются так, как я написал их выше; в принципе, их можно вычислить, двигаясь снизу вверх, и это именно то, чтоfoldl'
делает.)
По сути, это упражнение заставляет нас использовать foldr
вместо того foldl
соответствующим образом изменив ступенчатую функцию (поэтому мы используем s
вместо того f
) и начальный аккумулятор (поэтому мы используем ?
вместо того z
); список остается прежним, иначе о чем мы говорим?
Призыв к foldr
должен выглядеть так:
foldr s ? [1,2,3,4]
и соответствующая диаграмма такова:
_____what does the last call return?
/
s
/ \
1 s
/ \
2 s
/ \
3 s
/ \
4 ? <--- what is the initial accumulator?
Звонок приводит к
s 1 (s 2 (s 3 (s 4 ?)))
Что s
а также ?
? А какие у них виды? Это выглядит какs
это функция с двумя аргументами, очень похожая на f
, но не будем торопиться с выводами. Также оставим?
в сторону, и давайте заметим, что z
должен вступить в игру, как только 1
вступает в игру; однако как можноz
вступить в игру в призыве к аргументу может быть два s
функция, а именно в вызове s 1 (…)
? Мы можем решить эту часть загадки, выбравs
который принимает 3 аргумента, а не 2, о которых мы упоминали ранее, так что самый внешний вызов s 1 (…)
приведет к функции, принимающей один аргумент, который мы можем передать z
к!
Это означает, что нам нужен исходный вызов, который расширяется до
f (f (f (f z 1) 2) 3) 4
быть эквивалентным
s 1 (s 2 (s 3 (s 4 ?))) z
или, другими словами, мы хотим, чтобы частично примененная функция
s 1 (s 2 (s 3 (s 4 ?)))
быть эквивалентным следующей лямбда-функции
(\z -> f (f (f (f z 1) 2) 3) 4)
Опять же, "единственные" части, которые нам нужны, это s
а также ?
.
Поворотный момент: распознать состав функций
Давайте перерисуем предыдущую диаграмму и напишем справа, что мы хотим, чтобы каждый вызов s
быть эквивалентным:
s s 1 (…) == (\z -> f (f (f (f z 1) 2) 3) 4)
/ \
1 s s 2 (…) == (\z -> f (f (f z 2) 3) 4)
/ \
2 s s 3 (…) == (\z -> f (f z 3) 4)
/ \
3 s s 4 ? == (\z -> f z 4)
/ \
4 ? <--- what is the initial accumulator?
Надеюсь, из структуры диаграммы видно, что (…)
на каждой строке находится правая часть строки под ней; лучше, это функция, возвращенная из предыдущего (ниже) вызоваs
.
Также должно быть ясно, что вызов s
с аргументами x
а также y
является (полным) применением y
к частичному применению f
к единственному аргументу x
. Поскольку частичное применениеf
к x
можно записать как лямбда (\z -> f z x)
, полностью применяя y
к нему приводит лямбда (\z -> y (f z x))
, который в этом случае я бы переписал как y . (\z -> f z x)
; перевод слов в выражение дляs
мы получили
s x y = y . (\z -> f z x)
(Это то же самое, что и s x y z = y (f z x)
, который совпадает с книгой, если вы переименуете переменные.)
Последний бит: каково начальное "значение"?
аккумулятора? Приведенную выше диаграмму можно переписать, расширив вложенные вызовы, чтобы сделать их композиционными цепочками:
s s 1 (…) == (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2) . (\z -> f z 1)
/ \
1 s s 2 (…) == (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2)
/ \
2 s s 3 (…) == (\z -> f z 4) . (\z -> f z 3)
/ \
3 s s 4 ? == (\z -> f z 4)
/ \
4 ? <--- what is the initial accumulator?
Мы здесь видим, что s
просто "нагромождает" последовательные частичные применения f
, но y
в s x y = y . (\z -> f z x)
предполагает, что интерпретация s 4 ?
(и, в свою очередь, все остальные) пропускает ведущую функцию, которая должна быть составлена с самой левой лямбдой.
Это просто наш ?
функция: пришло время объяснить ему причину своего существования, помимо того, что он занимает место в вызове foldr
. Что мы можем выбрать, чтобы не менять результирующие функции? Ответ:id
, тождественный элемент относительно оператора композиции(.)
.
s s 1 (…) == id . (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2) . (\z -> f z 1)
/ \
1 s s 2 (…) == id . (\z -> f z 4) . (\z -> f z 3) . (\z -> f z 2)
/ \
2 s s 3 (…) == id . (\z -> f z 4) . (\z -> f z 3)
/ \
3 s s 4 id == id . (\z -> f z 4)
/ \
4 id
Итак, искомая функция
myFoldl f z xs = foldr (\x g a -> g (f a x)) id xs z
Я знаю, что этот вопрос довольно старый, но я хотел бы добавить другую точку зрения, которую я считаю полезной, и другие тоже могут это сделать.
Начав с небольшого примера, ключевой вывод состоит в том, чтобы заметить, что и действия в коротком списке можно соответственно переписать как
foldr f z [a, b, c]
== a `f` (b `f` (c `f` z))
== (a `f`) . (b `f`) . (c `f`) $ z
== (f a) . (f b) . (f c) $ z
и
foldl f z [a, b, c]
== ((z `f` a) `f` b) `f` c
== (`f` c) . (`f` b) . (`f` a) $ z
== ((flip f) c) . ((flip f) b) . ((flip f) a) $ z
Обратите внимание, что основное отличие состоит в том, чтоf
перевернут, и порядок композиции обратный.
Обычно мы думаем о сворачивании как о «обработке списка»; изменение точки зрения, за которое я выступаю, заключается в том, что вместо этого мы рассматриваем их как построение композиции функций, параметризованной списком .
Начнем с кажущегося отступления.
Простая композиция
Если у нас есть список функций, мы можем скомпоновать их все вместе и получить новую функцию. Реализация достаточно проста с точки зрения:
compose :: [a -> a] -> (a -> a)
compose = foldr (.) id
Если мы когда-нибудь захотим скомпоновать их в обратном порядке, мы, конечно, можем связать их в цепочку.compose . reverse
, но в качестве альтернативы мы можем напрямую определить операцию с, перевернув(.)
:
composeRev :: [a -> a] -> (a -> a)
composeRev = foldr (flip (.)) id
Параметризованная композиция
Допустим теперь, что вместо списка функций[a -> a]
в нашем распоряжении есть список[b]
и функцияb -> (a -> a)
, что для каждого элемента списка создается функцияa -> a
как указано выше. Мы можем сопоставить каждый элемент списка с соответствующей функцией, а затем составить список результирующих функций:
compMap :: (b -> (a -> a)) -> [b] -> (a -> a)
compMap f = compose . map f
Опять же, это можно реализовать напрямую с помощью as
compMap f = foldr (\x chain -> f x . chain) id
= foldr ((.) . f) id
Аналогично, чтобы составить обратную композицию, мы можем либо сделать
compMapRev :: (b -> (a -> a)) -> [b] -> (a -> a)
compMapRev f = composeRev . map f
= compose . map f . reverse
= compMap f . reverse
или напишите это с нуля в терминах as
compMapRev f = foldr (\x chain -> chain . f x) id
= foldr ((flip (.)) . f) id
Вернуться к складыванию
Оглядываясь назад на наши первоначальные небольшие примеры, мы можем почувствовать, что между / и / существует сильная связь.
Сходство между и станет более очевидным, если мы выровняем их сигнатуры типов:
foldr :: (b -> (a -> a)) -> a -> [b] -> a
compMap :: (b -> (a -> a)) -> [b] -> a -> a
Мы сразу понимаем, чтоfoldr
иcompMap
просто поменяйте местами второй и третий аргументы, т.е.foldr f == flip (compMap f)
, то естьfoldr == flip . compMap
.
Давайте посмотрим, сможем ли мы сделать то же самое сfoldl
. На этот раз мы должны не забыть сравнить его сcompMapRev
вместо этого, потому что порядок композиции должен быть таким же:
foldl :: (a -> b -> a) -> a -> [b] -> a
compMapRev :: (b -> (a -> a)) -> [b] -> a -> a
Помимо замены второго и третьего аргументов, на этот раз также переворачивается первый аргумент функции, т.е.foldl f == flip (compMapRev (flip f))
, то естьfoldl == flip . compMapRev . flip
.
Из предыдущих наблюдений следует, что выполняются следующие тождества:
foldr f z list = compMap f list z
== foldr ((.) . f) id list z -- does not work as a recursive definition
foldl f z list = compMapRev (flip f) list z
= foldr ((flip (.)) . (flip f)) id list z
Это последнее, что мы искали все время.
Мы также можем упростить и согласовать две идентичности, чтобы подчеркнуть сходство:
foldr f == flip $ foldr ( (.) . f ) id -- does not work as a definition
foldl f = flip $ foldr ((flip (.)) . (flip f)) id
Это может помочь, я попытался расширить по-другому.
myFoldl (+) 0 [1,2,3] =
foldr step id [1,2,3] 0 =
foldr step (\a -> id (a+3)) [1,2] 0 =
foldr step (\b -> (\a -> id (a+3)) (b+2)) [1] 0 =
foldr step (\b -> id ((b+2)+3)) [1] 0 =
foldr step (\c -> (\b -> id ((b+2)+3)) (c+1)) [] 0 =
foldr step (\c -> id (((c+1)+2)+3)) [] 0 =
(\c -> id (((c+1)+2)+3)) 0 = ...
foldr step zero (x:xs) = step x (foldr step zero xs)
foldr _ zero [] = zero
myFold f z xs = foldr step id xs z
where step x g a = g (f a x)
myFold (+) 0 [1, 2, 3] =
foldr step id [1, 2, 3] 0
-- Expanding foldr function
step 1 (foldr step id [2, 3]) 0
step 1 (step 2 (foldr step id [3])) 0
step 1 (step 2 (step 3 (foldr step id []))) 0
-- Expanding step function if it is possible
step 1 (step 2 (step 3 id)) 0
step 2 (step 3 id) (0 + 1)
step 3 id ((0 + 1) + 2)
id (((0 + 1) + 2) + 3)
Ну, по крайней мере, это помогло мне. Даже это не совсем верно.
Этот ответ позволяет легко понять приведенное ниже определение за три шага.
-- file: ch04/Fold.hs
myFoldl :: (a -> b -> a) -> a -> [b] -> a
myFoldl f z xs = foldr step id xs z
where step x g a = g (f a x)
Шаг 1. Преобразуйте кратность оценки функции в комбинацию функций
foldl f z [x1 .. xn] = z & f1 & .. & fn = fn . .. . f1 z
. в которомfi = \z -> f z xi
.
(Используя z & f1 & f2 & .. & fn
это означает fn ( .. (f2 (f1 z)) .. )
.)
Шаг 2. выразите комбинацию функций в виде foldr
манера
foldr (.) id [f1 .. fn] = (.) f1 (foldr (.) id [f2 .. fn]) = f1 . (foldr (.) id [f2 .. fn])
. Разверните остальное, чтобы получитьfoldr (.) id [f1 .. fn] = f1 . .. . fn
.
Заметив, что последовательность обратная, мы должны использовать обратную форму (.)
. Определитьrc f1 f2 = (.) f2 f1 = f2 . f1
, тогда foldr rc id [f1 .. fn] = rc f1 (foldr (.) id [f2 .. fn]) = (foldr (.) id [f2 .. fn]) . f1
. Разверните остальное, чтобы получитьfoldr rc id [f1 .. fn] = fn . .. . f1
.
Шаг 3. Преобразуйте свертку в списке функций в свертку в списке операндов.
найти step
что делает foldr step id [x1 .. xn] = foldr rc id [f1 .. fn]
. Легко найтиstep = \x g z -> g (f z x)
.
За 3 шага определение foldl
с помощью foldr
чисто:
foldl f z xs
= fn . .. . f1 z
= foldr rc id fs z
= foldr step id xs z
Докажите правильность:
foldl f z xs = foldr (\x g z -> g (f z x)) id xs z
= step x1 (foldr step id [x2 .. xn]) z
= s1 (foldr step id [x2 .. xn]) z
= s1 (step x2 (foldr step id [x3 .. xn])) z
= s1 (s2 (foldr step id [x3 .. xn])) z
= ..
= s1 (s2 (.. (sn (foldr step id [])) .. )) z
= s1 (s2 (.. (sn id) .. )) z
= (s2 (.. (sn id) .. )) (f z x1)
= s2 (s3 (.. (sn id) .. )) (f z x1)
= (s3 (.. (sn id) .. )) (f (f z x1) x2)
= ..
= sn id (f (.. (f (f z x1) x2) .. ) xn-1)
= id (f (.. (f (f z x1) x2) .. ) xn)
= f (.. (f (f z x1) x2) .. ) xn
in which xs = [x1 .. xn], si = step xi = \g z -> g (f z xi)
Если вы обнаружите, что что-то неясно, добавьте комментарий.:)