it-swarm.dev

Написание фолдла с использованием фолдра

В Real World Haskell, Глава 4. Функциональное программирование

Напишите foldl с помощью foldr:

-- 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 параметра, я совершенно запутался!

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

70
ylzhang

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

Для чего нужна функция id? Какова роль? Зачем нам это нужно здесь?

id - это функция идентификации , id x = x и используется как эквивалент нуля при построении цепочки функций с композиция функций , (.). Вы можете найти это определено в Prelude .

В приведенном выше примере функция 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) [ ]

Это определение более эффективно, чем наше первоначальное определение с использованием fold, поскольку оно позволяет избежать использования неэффективного оператора добавления (++) для списков.

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

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

То есть, задана функция f типа α → β → β и значение v типа β, функция fold f v обрабатывает список типа [α], чтобы дать значение типа β путем замены всухую конструктор [] в конце списка по значению v и каждый конструктор cons (:) в списке с помощью функции f. Таким образом, оператор fold инкапсулирует простой шаблон рекурсии для обработки списков, в котором два конструктора для списков просто заменяются другими значениями и функциями. Ряд знакомых функций в списках имеет простое определение с использованием fold.

Это похоже на рекурсивную схему, очень похожую на нашу функцию g. Теперь хитрость: используя всю доступную магию (Bird, Meertens и Malcolm), мы применяем специальное правило, универсальное свойство сгиба , которое является эквивалентностью между двумя определениями для функции 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 в каждом элементе списка, в обратном порядке (поэтому мы сворачиваем влево, а не вправо).

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


ссылки

91
Don Stewart

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

9
C. A. McCann

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

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

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

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

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

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

Заключение

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

5
Mirzhan Irkegulov

Вот мое доказательство того, что 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. Есть два случая, чтобы выяснить, является ли myfoldlfoldl:

  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.

4
David

Нет никакой Королевской Дороги к Математике, ни даже через 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 или что?

1
disznoperzselo

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

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
Dulguun Otgon
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)

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

0
hanrai