Реализовать zip с помощью foldr

в настоящее время я нахожусь на главе 4 реального мира Haskell, и я пытаюсь обернуть голову вокруг реализация foldl с точки зрения foldr.

(вот их код:)

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)

Я думал, что попытаюсь реализовать zip используя ту же технику, но я, похоже, не делаю никакого прогресса. Это вообще возможно?

7 ответов


zip2 xs ys = foldr step done xs ys
  where done ys = []
        step x zipsfn []     = []
        step x zipsfn (y:ys) = (x, y) : (zipsfn ys)

как это работает: (шаг foldr done xs) возвращает функцию, которая потребляет йс; поэтому спускаемся хз списка наращивание вложенного состав функции, которые будут применяться к соответствующей части ys.

как это придумать: я начал с общей идеи (из аналогичного примеры видел раньше), написал

zip2 xs ys = foldr step done xs ys

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

первая строка может быть написана более просто как

zip2 = foldr step done

как показал маттиаст.


ответ уже был дан здесь, но не (наглядный) деривации. Так что даже после всех этих лет, возможно, стоит добавить.

это на самом деле довольно просто. Во-первых,

foldr f z xs 
   = foldr f z [x1,x2,x3,...,xn] = f x1 (foldr f z [x2,x3,...,xn])
   = ... = f x1 (f x2 (f x3 (... (f xn z) ...)))

следовательно, eta-расширением,

foldr f z xs ys
   = foldr f z [x1,x2,x3,...,xn] ys = f x1 (foldr f z [x2,x3,...,xn]) ys
   = ... = f x1 (f x2 (f x3 (... (f xn z) ...))) ys

как видно здесь,если f не является принудительным в своем 2-м аргументе, он начинает работать первый on x1 и ys, f x1r1ys где r1 =(f x2 (f x3 (... (f xn z) ...)))= foldr f z [x2,x3,...,xn].

Итак, используя

f x1 r1 [] = []
f x1 r1 (y1:ys1) = (x1,y1) : r1 ys1

организуем прохождение информации слева направо по списку, by вызов r1 с остальное в список ys1, foldr f z [x2,x3,...,xn]ys1 = f x2r2ys1, как следующий шаг. Вот и все.


, когда ys меньше, чем xs (или такая же длина), то [] чехол для f пожары и обработка останавливается. Но если ... --12--> больше, чем xs затем f ' s [] дело не будет стрелять, и мы доберемся до финала f xnz(yn:ysn) приложения

f xn z (yn:ysn) = (xn,yn) : z ysn

так как мы достигли конца xs на zip обработка должна прекратиться:

z _ = []

и это означает, что определение z = const [] следует использовать:

zip xs ys = foldr f (const []) xs ys
  where
    f x r []     = []
    f x r (y:ys) = (x,y) : r ys

от точка зрения f, r играет роль успех продолжения, который f вызовы, когда обработка должна продолжаться, после испускания пары (x,y).

так r и " что делается с more ys когда есть более xs" и z = const [] на nil-дело в foldr, is "то, что делается с ys когда больше нет xs". Или f можете заехать сам, возвращаясь [], когда ys исчерпан.


обратите внимание, как ys используется как своего рода накопительное значение, которое передается слева направо по списку xs, от вызова f к следующему ("аккумулируя" шаг, здесь, обнажая головной элемент от его).

естественно это соответствует левой складке, где накапливается шаг "применение функции", с z = id возвращение финал накопленное значение, когда "больше нет xs":

foldl f a xs =~ foldr (\x r a-> r (f a x)) id xs a

аналогично, для конечных списков,

foldr f a xs =~ foldl (\r x a-> r (f x a)) id xs a

и поскольку комбинирующая функция решает, продолжать или нет, теперь можно оставить складку, которая может остановиться раньше:

foldlWhile t f a xs = foldr cons id xs a
  where 
    cons x r a = if t x then r (f a x) else a

или пропуск левой складки,foldlWhen t ... с

    cons x r a = if t x then r (f a x) else r a

etc.


Я нашел способ, используя довольно похожий метод на ваш:

myzip = foldr step (const []) :: [a] -> [b] -> [(a,b)]
    where step a f (b:bs) = (a,b):(f bs)
          step a f [] = []

для неродных Haskellers здесь, я написал версию схемы этого алгоритма, чтобы сделать его более ясным, что на самом деле происходит:

> (define (zip lista listb)
    ((foldr (lambda (el func)
           (lambda (a)
             (if (empty? a)
                 empty
                 (cons (cons el (first a)) (func (rest a))))))
         (lambda (a) empty)
         lista) listb))
> (zip '(1 2 3 4) '(5 6 7 8))
(list (cons 1 5) (cons 2 6) (cons 3 7) (cons 4 8))

на foldr приводит к функции, которая при применении к списку возвращает zip списка, сложенного со списком, заданным функции. Хаскелл скрывает внутреннее lambda из-за ленивости.


чтобы разбить его дальше:

take zip on input: '(1 2 3) Эти выражения производится Функ получает вызов с

el->3, func->(lambda (a) empty)

это расширяет:

(lambda (a) (cons (cons el (first a)) (func (rest a))))
(lambda (a) (cons (cons 3 (first a)) ((lambda (a) empty) (rest a))))

если бы мы должны были вернуть это сейчас, у нас была бы функция, которая принимает список из одного элемента и возвращает пару (3 элемента):

> (define f (lambda (a) (cons (cons 3 (first a)) ((lambda (a) empty) (rest a)))))
> (f (list 9))
(list (cons 3 9))

продолжая, foldr теперь вызывает func с

el->3, func->f ;using f for shorthand
(lambda (a) (cons (cons el (first a)) (func (rest a))))
(lambda (a) (cons (cons 2 (first a)) (f (rest a))))

это функция, которая берет список с двумя элементами, теперь, и застегивает их с (list 2 3):

> (define g (lambda (a) (cons (cons 2 (first a)) (f (rest a)))))
> (g (list 9 1))
(list (cons 2 9) (cons 3 1))

что происходит?

(lambda (a) (cons (cons 2 (first a)) (f (rest a))))

a в этом случае, is (list 9 1)

(cons (cons 2 (first (list 9 1))) (f (rest (list 9 1))))
(cons (cons 2 9) (f (list 1)))

и, как вы помните, f молнии на аргументе 3.

и это продолжается и т. д...


проблема со всеми этими решениями zip заключается в том, что они складываются только над одним списком или другим, что может быть проблемой, если оба они являются "хорошими производителями", на языке слияния списков. На самом деле вам нужно решение, которое складывается над обоими списками. К счастью, именно об этом есть статья под названием "Корутинговые складки с Гиперфункциями".

вам нужен вспомогательный тип, гиперфункция, которая в основном является функцией, которая принимает другой гиперфункция в качестве аргумента.

newtype H a b = H { invoke :: H b a -> b }

гиперфункции, используемые здесь, в основном действуют как "стек" обычных функций.

push :: (a -> b) -> H a b -> H a b
push f q = H $ \k -> f $ invoke k q

Вам также нужен способ объединить две гиперфункции, конец в конец.

(.#.) :: H b c -> H a b -> H a c
f .#. g = H $ \k -> invoke f $ g .#. k

это связано с push по закону:

(push f x) .#. (push g y) = push (f . g) (x .#. y)

это оказывается ассоциативным оператором, и это тождество:

self :: H a a
self = H $ \k -> invoke k self

Вам также нужно что-то, что игнорирует все остальное "стек" и возвращает определенное значение:

base :: b -> H a b
base b = H $ const b

и, наконец, вам нужен способ получить значение из гиперфункции:

run :: H a a -> a
run q = invoke q self

run строк все pushed функции вместе, конец в конец, пока он не попадает в base или петли бесконечно.

Итак, теперь вы можете сложить оба списка в гиперфункции, используя функции, которые передают информацию от одного к другому, и собрать конечное значение.

zip xs ys = run $ foldr (\x h -> push (first x) h) (base []) xs .#. foldr (\y h -> push (second y) h) (base Nothing) ys where
  first _ Nothing = []
  first x (Just (y, xys)) = (x, y):xys

  second y xys = Just (y, xys)

причина сворачивание обоих списков имеет значение из-за того, что GHC делает под названием список fusion, о котором говорится в GHC.Базовый модуль, но, вероятно, должно быть гораздо более известным. Быть хорошим производителем списка и использовать build С foldr может предотвратить много бесполезного производства и немедленного потребления элементов списка и может подвергнуть дальнейшей оптимизации.


я сам пытался понять это элегантное решение, поэтому я сам попытался вывести типы и оценку. Итак, нам нужно написать функцию:

zip xs ys = foldr step done xs ys

здесь нам нужно вывести step и done, что бы это ни было. Вспомните foldrтип, созданный для списков:

foldr :: (a -> state -> state) -> state -> [a] -> state
наши foldr вызов должен быть создан для чего - то вроде ниже, потому что мы должны принять не один, а два списка аргументы:
foldr :: (a -> ? -> ?) -> ? -> [a] -> [b] -> [(a,b)]

, потому что -> is право-ассоциативной, это эквивалентно:

foldr :: (a -> ? -> ?) -> ? -> [a] -> ([b] -> [(a,b)])

наши ([b] -> [(a,b)]) соответствует state введите переменную в исходном foldr введите подпись, поэтому мы должны заменить каждое вхождение state С

foldr :: (a -> ([b] -> [(a,b)]) -> ([b] -> [(a,b)]))
      -> ([b] -> [(a,b)])
      -> [a]
      -> ([b] -> [(a,b)])

это означает, что аргументы, которые мы передаем foldr должны иметь следующие типы:

step :: a -> ([b] -> [(a,b)]) -> [b] -> [(a,b)]
done :: [b] -> [(a,b)]
xs :: [a]
ys :: [b]

Напомним, что foldr (+) 0 [1,2,3] расширяет кому:

1 + (2 + (3 + 0))

таким образом, если xs = [1,2,3] и ys = [4,5,6,7], наш foldr вызов расширится до:

1 `step` (2 `step` (3 `step` done)) $ [4,5,6,7]

это значит, что наши 1 `step` (2 `step` (3 `step` done)) construct должен создать рекурсивную функцию, которая будет проходить через [4,5,6,7] и застегнуть элементы. (Имейте в виду, что если один из исходных списков длиннее, избыточные значения выбрасываются). IOW, наша конструкция должна иметь тип [b] -> [(a,b)].

3 `step` done наш базовый случай, где done начальный значение, как 0 на foldr (+) 0 [1..3]. Мы не хотим ничего zip после 3, потому что 3 является конечным значением xs, поэтому мы должны прекратить рекурсию. Как вы завершаете рекурсию над списком в базовом случае? Вы возвращаете пустой список []. Но вспомните!--15--> подпись типа:

done :: [b] -> [(a,b)]

поэтому мы не можем вернуться просто [], мы должны вернуть функцию, которая будет игнорировать все, что она получает. Поэтому используйте const:

done = const [] -- this is equivalent to done = \_ -> []

теперь давайте начнем выяснять, что step должно быть. Он сочетает в себе значение типа a функции типа [b] -> [(a,b)] и возвращает функцию типа [b] -> [(a,b)].

на 3 `step` done, мы знаем, что значение результата, которое позже перейдет в наш список zipped, должно быть (3,6) (зная от оригинала xs и ys). Поэтому 3 `step` done должны оценить в:

\(y:ys) -> (3,y) : done ys

помните, мы должны вернуть функция, внутри которой мы как-то застегиваем элементы, приведенный выше код имеет смысл и typechecks.

теперь, когда мы предположили, как именно step следует оценить, давайте продолжим оценку. Вот как все шаги сокращения в нашем foldr оценка выглядит так:

3 `step` done -- becomes
(\(y:ys) -> (3,y) : done ys)
2 `step` (\(y:ys) -> (3,y) : done ys) -- becomes
(\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys)
1 `step` (\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys) -- becomes
(\(y:ys) -> (1,y) : (\(y:ys) -> (2,y) : (\(y:ys) -> (3,y) : done ys) ys) ys)

оценка приводит к этой реализации шага (обратите внимание, что мы учитываем ys заканчивается элементов рано, возвращая пустой список):

step x f = \[] -> []
step x f = \(y:ys) -> (x,y) : f ys

таким образом, полная функция zip реализуется следующим образом:

zip :: [a] -> [b] -> [(a,b)]
zip xs ys = foldr step done xs ys
  where done = const []
        step x f [] = []
        step x f (y:ys) = (x,y) : f ys

P. S.: Если вы вдохновлены изяществом складок, читайте:!--94-->написание foldl с помощью foldr а потом Грэма Хаттона учебник по универсальности и выразительности раза.


простой подход:

lZip, rZip :: Foldable t => [b] -> t a -> [(a, b)]

-- implement zip using fold?
lZip xs ys = reverse.fst $ foldl f ([],xs) ys
     where f  (zs, (y:ys)) x = ((x,y):zs, ys)

-- Or;
rZip xs ys = fst $ foldr f ([],reverse xs) ys
     where f x (zs, (y:ys))  = ((x,y):zs, ys)