Написание фолдла с использованием - PullRequest
71 голосов
/ 30 мая 2011

In Real World 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

Но я все еще не могу полностью понять это, вот мои вопросы:

  1. Для чего нужна функция id? Какова роль? Зачем нам это нужно здесь?
  2. В приведенном выше примере функция id является аккумулятором в лямбда-функции?
  3. Прототип Foldr равен foldr :: (a -> b -> b) -> b -> [a] -> b, и первый параметр - это функция, для которой нужны два параметра, но пошаговая функция в реализации myFoldl использует 3 параметра, я совершенно запутался!

Кто-нибудь может мне помочь? Большое спасибо!

Ответы [ 7 ]

91 голосов
/ 30 мая 2011

Некоторые пояснения приведены по порядку!

Для чего нужна функция 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).

* 1067Прототип * foldr - это foldr :: (a -> b -> b) -> b -> [a] -> b

Программист на Haskell скажет, что type из 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), а позже к вставкеоператор ФП (Бакус, 1978).В Haskell оператор fold для списков может быть определен следующим образом:

fold :: (α → β → β) → β → ([α] → β)
fold f v [ ] = v
fold f v (x : xs) = f x (fold f v xs)

То есть с учетом функции f типа α → β → β и значения v типа βфункция fold f v обрабатывает список типа [α] для получения значения типа β, заменяя nil constructor [] в конце списка по значению v, а каждый конструктор cons (:) в списке по функции 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 as:

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 для каждого элемента списка, в обратном порядке.(поэтому мы сворачиваем влево вместо права).

Это определенно несколько продвинуто, поэтому, чтобы глубоко понять это преобразование, универсальное свойство складок , которое делает возможным преобразование, я рекомендую Hutton'sучебник, ссылка ниже.


Список литературы

9 голосов
/ 30 мая 2011

Рассмотрим тип 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.

5 голосов
/ 27 февраля 2015

(быстро просмотрите мои ответы [1] , [2] , [3] , [4] чтобы убедиться, что вы понимаете синтаксис Haskell, функции высшего порядка, карри, составление функций, оператор $, операторы инфикса / префикса, разделы и лямбды)

Универсальное свойство сгиба

A fold - это просто кодификация определенных видов рекурсии. А свойство универсальности просто утверждает, что, если ваша рекурсия соответствует определенной форме, она может быть преобразована в складку в соответствии с некоторыми формальными правилами. И наоборот, каждая складка может быть преобразована в рекурсию такого рода. Еще раз, некоторые рекурсии могут быть переведены в сгибы, которые дают точно такой же ответ, а некоторые рекурсии не могут, и для этого существует точная процедура.

По сути, если ваша рекурсивная функция работает со списками, похожими на влево , вы можете преобразовать ее, чтобы сложить один вправо , подставив 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

Упражнение:

  1. Реализуйте map, filter, reverse, concat и concatMap рекурсивно, как и вышеупомянутые функции на левой стороне.

  2. Преобразуйте эти 5 функций в сложение в соответствии с формулой выше , то есть подставьте f и v в формулу сгиба в вправо .

Foldl via 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

Заключение * +1101 * Теперь прочитайте Грэма Хаттона * Учебник об универсальности и выразительности сгиба . Возьми ручку и бумагу, постарайся изобразить все, что он пишет, пока ты сам не получишь большинство складок. Не парьтесь, если что-то не понимаете, вы всегда можете вернуться позже, но тоже не откладывайте на потом.

4 голосов
/ 10 октября 2012

Вот мое доказательство того, что 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:

  1. Базовый случай: пустой список

      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)
    
  2. Непустой список

      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
    

Так как в 2. первая и последняя строки имеют одинаковую форму в обоих случаях, ее можно использовать для сворачивания списка до xs == [], и в этом случае 1. гарантирует один и тот же результат. Итак, по индукции, myfoldl == foldl.

1 голос
/ 21 января 2018

Это может помочь, я попытался расширить по-другому.

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 = ...
1 голос
/ 24 июня 2014

Королевской дороги к математике нет, даже через Хаскелл.Пусть

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:

= foldl f z [x0, x1, x2]

Это Royal Road или что?

0 голосов
/ 01 декабря 2018
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)

Ну, по крайней мере, это помогло мне. Даже это не совсем верно.

...