Как два продолжения могут компенсировать друг друга?
Я читаю некоторые хитрости для манипулирования списками, и он содержит следующее:
zipRev xs ys = foldr f id xs snd (ys,[])
where
f x k c = k (\((y:ys),r) -> c (ys,(x,y):r))
Здесь мы можем видеть, что у нас есть два продолжения, сложенных друг на друга. Когда это происходит, они часто могут "отменить", например так:
zipRev xs ys = snd (foldr f (ys,[]) xs)
where
f x (y:ys,r) = (ys,(x,y):r)
Я не понимаю, как вы "отменяете" сложенные продолжения, чтобы перейти от верхнего блока кода к нижнему. Какой шаблон вы ищете, чтобы сделать это преобразование, и почему оно работает?
Ответы
Ответ 1
Функция f :: a → b
может быть "замаскирована" внутри двойного продолжения как функция f' :: ((a → r1) → r2) → ((b → r1) → r2)
.
obfuscate :: (a -> b) -> ((a -> r1) -> r2) -> (b -> r1) -> r2
obfuscate f k2 k1 = k2 (k1 . f)
obfuscate
есть приятное свойство: он сохраняет композицию функций и идентичность: вы можете доказать, что obfuscate f. obfuscate g === obfuscate (f. g)
obfuscate f. obfuscate g === obfuscate (f. g)
и этот obfuscate id === id
в несколько шагов. Это означает, что вы часто можете использовать это преобразование, чтобы распутать вычисления с двойным продолжением, которые составляют obfuscate
d-функции вместе, вычленяя obfuscate
из композиции. Этот вопрос является примером такого распутывания.
f
в верхнем блоке кода - это obfuscate
d-версия f
в нижнем блоке (точнее, top fx
- это obfuscate
d-версия нижнего fx
). Вы можете увидеть это, заметив, как top f
применяет внешнее продолжение к функции, которая преобразует свои входные данные, а затем применяет все это к внутреннему продолжению, так же, как в теле obfuscate
.
Итак, мы можем начать распутывать zipRev
:
zipRev xs ys = foldr f id xs snd (ys,[])
where
f x = obfuscate (\(y:ys,r) -> (ys,(x,y):r))
Поскольку действие foldr
здесь состоит в том, чтобы составить кучу obfuscate
d-функций друг с другом (и применить все это к id
, который мы можем оставить справа), мы можем разложить obfuscate
на внешнюю сторону всей складки:
zipRev xs ys = obfuscate (\accum -> foldr f accum xs) id snd (ys,[])
where
f x (y:ys,r) = (ys,(x,y):r)
Теперь примените определение obfuscate
и упростите:
zipRev xs ys = obfuscate (\accum -> foldr f accum xs) id snd (ys,[])
zipRev xs ys = id (snd . (\accum -> foldr f accum xs)) (ys,[])
zipRev xs ys = snd (foldr f (ys,[]) xs)
QED!
Ответ 2
Учитывая функцию
g :: a₁ -> a₂
мы можем поднять его до функции по продолжениям, переключив порядок:
lift g = (\c a₁ -> c (g a₁))
:: (a₂ -> t) -> a₁ -> t
Это преобразование является контравариантным функтором, то есть взаимодействует с композицией функций, изменяя ее порядок:
g₁ :: a₁ -> a₂
g₂ :: a₂ -> a₃
lift g₁ . lift g₂
== (\c₁ a₁ -> c₁ (g₁ a₁)) . (\c₂ a₂ -> c₂ (g₂ a₂))
== \c₂ a₁ -> (\a₂ -> c₂ (g₂ a₂)) (g₁ a₁)
== \c₂ a₁ -> c₂ (g₂ (g₁ a₁))
== lift (g₂ . g₁)
:: (a₃ -> t) -> a₁ -> t
lift id
== (\c a₁ -> c a₁)
== id
:: (a₁ -> t) -> a₁ -> t
Мы можем снова поднять поднятую функцию тем же способом, что и функция на сложенных продолжениях, с обратным порядком:
lift (lift g)
== (\k c -> k ((\c a₁ -> c (g a₁)) c))
== (\k c -> k (\a₁ -> c (g a₁)))
:: ((a₁ -> t) -> u) -> (a₂ -> t) -> u
Суммирование двух контравариантных функторов дает нам (ковариантный) функтор:
lift (lift g₁) . lift (lift g₂)
== lift (lift g₂ . lift g₁)
== lift (lift (g₁ . g₂))
:: ((a₁ -> t) -> u) -> (a₃ -> t) -> u
lift (lift id)
== lift id
== id
:: ((a₁ -> t) -> u) -> (a₁ -> t) -> u
Это как раз обратное преобразование в вашем примере с g = \(y:ys, r) → (ys, (x, y):r)
. Этот g
является эндоморфизмом (a₁ = a₂
), и foldr
составляет его копии с различными x
. То, что делали, - это замена композиции функций с двойным поднятием на функцию двойного лифта композиции функций, что является просто индуктивным применением законов функторов:
f :: x -> a₁ -> a₁
c :: (a₁ -> t) -> u
xs :: [x]
foldr (\x -> lift (lift (f x))) c xs
== lift (lift (\a₁ -> foldr f a₁ xs)) c
:: (a₁ -> t) -> u
Ответ 3
Давайте попробуем разобраться в этом коде с элементарной точки зрения. Что это вообще делает, интересно?
zipRev xs ys = foldr f id xs snd (ys,[])
where
-- f x k c = k (\(y:ys, r) -> c (ys, (x,y):r))
f x k c = k (g x c)
-- = (k . g x) c -- so,
-- f x k = k . g x
g x c (y:ys, r) = c (ys, (x,y):r)
Здесь мы использовали лямбда-лифтинг для восстановления g
комбинатора.
Итак, потому что fxk = k. gx
fxk = k. gx
k
идет слева от x
, список ввода переводится в обратную цепочку композиций,
foldr f id [x1, x2, x3, ..., xn] where f x k = k . g x
===>>
(((...(id . g xn) . ... . g x3) . g x2) . g x1)
и, таким образом, он просто делает то, что сделал бы левый сгиб,
zipRev [] ys = []
zipRev [x1, x2, x3, ..., xn] ys
= (id . g xn . ... . g x3 . g x2 . g x1) snd (ys, [])
= g xn (g xn1 ( ... ( g x3 ( g x2 ( g x1 snd)))...)) (ys, [])
where ----c--------------------------------------------
g x c (y:ys, r) = c (ys, (x,y):r)
Итак, мы перешли к глубокому концу списка xs
, а затем возвращаемся, используя список ys
слева направо (т.е. сверху вниз), на обратном пути справа налево в списке xs
(то есть снизу вверх).). Это прямо закодировано как правый сгиб со строгим редуктором, поэтому поток действительно справа налево на xs
. Самое нижнее действие (snd
) в цепочке выполняется последним, поэтому в новом коде оно становится самым верхним (все еще выполняется последним):
zipRev xs ys = snd (foldr h (ys,[]) xs)
where
h x (y:ys, r) = (ys, (x,y):r)
gxc
использовался как продолжение в исходном коде, а c
как продолжение второго уровня; но на самом деле все это было обычным сгибом справа.
Так что это действительно застегивает обратный первый список со вторым. Это также небезопасно; он пропускает пункт:
g x c ([], r) = c ([], r) -- or just 'r'
g x c (y:ys, r) = c (ys, (x,y):r)
(обновление :) Ответы от duplode (и Joseph Sible) выполняют лямбда-подъем немного по-другому, что лучше подходит для этой задачи. Это выглядит так:
zipRev xs ys = foldr f id xs snd (ys,[])
where
f x k c = k (\((y:ys), r) -> c (ys, (x,y):r))
= k (c . (\((y:ys), r) -> (ys, (x,y):r)) )
= k (c . g x)
g x = (\((y:ys), r) -> (ys, (x,y):r))
{- f x k c = k ((. g x) c) = (k . (. g x)) c = (. (. g x)) k c
f x = (. (. g x)) -}
тогда
foldr f id [ x1, x2, ... , xn ] snd (ys,[]) =
= ( (. (. g x1)) $ (. (. g x2)) $ ... $ (. (. g xn)) id ) snd (ys,[]) -- 1,2...n
= ( id . (. g xn) . ... . (. g x2) . (. g x1) ) snd (ys,[]) -- n...2,1
= ( snd . g x1 . g x2 . ... . g xn ) (ys,[]) -- 1,2...n!
= snd $ g x1 $ g x2 $ ... $ g xn (ys,[])
= snd $ foldr g (ys,[]) [x1, x2, ..., xn ]
Просто. :) Дважды щелкнуть - не совсем
Ответ 4
Давайте начнем с нескольких косметических корректировок:
-- Note that 'g x' is an endomorphism.
g :: a -> ([b], [(a,b)]) -> ([b], [(a,b)])
g x ((y:ys),r) = (ys,(x,y):r)
zipRev xs ys = foldr f id xs snd (ys,[])
where
f x k = \c -> k (c . g x)
f
передает продолжение (c. gx
) в другую функцию (k
, как говорит пользователь 11212628, "двойное продолжение").
Хотя можно разумно ожидать, что повторное использование f
в процессе сгибания каким-то образом будет составлять gx
эндоморфизмы, составленные из элементов списка, порядок, в котором составлены эндоморфизмы, может быть не сразу очевиден, поэтому нам лучше пройтись по несколько шагов, чтобы быть уверенным:
-- x0 is the first element, x1 the second, etc.
f x0 k0
\c -> k0 (c . g x0)
\c -> (f x1 k1) (c . g x0) -- k0 is the result of a fold step.
\c -> (\d -> k1 (d . g x1)) (c . g x0) -- Renaming a variable for clarity.
\c -> k1 (c . g x0 . g x1)
-- etc .
-- xa is the *last* element, xb the next-to-last, etc.
-- ka is the initial value passed to foldr.
\c -> (f xa ka) (c . g x0 . g x1 . . . g xb)
\c -> (\d -> ka (d . g xa)) (c . g x0 . g x1 . . . g xb)
\c -> ka (c . g x0 . g x1 . . . g xb . g xa)
ka
, начальное значение, передаваемое в foldr, является id
, что немного упрощает задачу:
foldr f id xs = \c -> c . g x0 . g x1 . . . g xa
Так как все, что мы делаем с аргументом c
переданным в foldr f id xs
это пост-компоновка его с эндоморфизмами, мы могли бы также выделить его из сгиба:
zipRev xs ys = (snd . foldr h id xs) (ys,[])
where
h x e = g x . e
Обратите внимание, как мы ушли от c. gx
c. gx
до gx. e
gx. e
. Возможно, это можно описать как побочный эффект от обмана CPS в первоначальной реализации.
Последний шаг - заметить, как hxe = gx. e
hxe = gx. e
точно соответствует тому, что мы сделали бы для реализации foldr
с точки зрения foldMap
для моноида Endo
. Или, если выразить это более явно:
foldEndo g i xs = foldr g i xs -- The goal is obtaining an Endo-like definition.
foldEndo _ i [] = i
foldEndo g i (x : xs) = g x (foldEndo g i xs)
foldEndo g i xs = go xs i
where
go [] = \j -> j
go (x : xs) = \j -> g x (foldEndo g j xs)
foldEndo g i xs = go xs i
where
go [] = \j -> j
go (x : xs) = \j -> g x (go xs j)
foldEndo g i xs = go xs i
where
go [] = id
go (x : xs) = g x . go xs
foldEndo g i xs = go xs i
where
h x e = g x . e
go [] = id
go (x : xs) = h x (go xs)
foldEndo g i xs = go xs i
where
h x e = g x . e
go xs = foldr h id xs
foldEndo g i xs = foldr h id xs i
where
h x e = g x . e
Это в конечном итоге приводит нас к тому, что мы искали:
zipRev xs ys = snd (foldr g (ys,[]) xs)
Ответ 5
Ответ user11228628 привел меня к пониманию. Вот несколько идей, которые я получил во время чтения, и некоторые пошаговые преобразования.
Insights
- Продолжения не отменяются напрямую. Они могут быть в конечном итоге отменены (путем бета-сокращения), потому что их можно выделить.
- Шаблон, который вы ищете, чтобы выполнить это преобразование:
\kc → k (c. f)
(или если вы любите нечитабельную pointfree, (. (. f))
) для любого f
(обратите внимание, что f
не является параметр к лямбде). - Как указывает duplode в комментарии, функции стиля с продолжением можно рассматривать как функтор, а
obfuscate
- их определение fmap
. - Уловка вытаскивания такой функции из
foldr
работает для любой функции, которая может быть допустимым fmap
.
Полное преобразование из первого блока кода во второй
zipRev xs ys = foldr f id xs snd (ys,[])
where
f x k c = k (\((y:ys),r) -> c (ys,(x,y):r))
Вытащите c
из лямбды
zipRev xs ys = foldr f id xs snd (ys,[])
where
f x k c = k (c . \((y:ys),r) -> (ys,(x,y):r))
Заменить obfuscate
для его определения
zipRev xs ys = foldr f id xs snd (ys,[])
where
f x = obfuscate (\((y:ys),r) -> (ys,(x,y):r))
Вытащите obfuscate
из лямбды
zipRev xs ys = foldr f id xs snd (ys,[])
where
f = obfuscate . \x ((y:ys),r) -> (ys,(x,y):r)
Вытащить obfuscate
из f
zipRev xs ys = foldr (obfuscate . f) id xs snd (ys,[])
where
f x ((y:ys),r) = (ys,(x,y):r)
Так как obfuscate
следует законам функторов, мы можем вытащить его из foldr
zipRev xs ys = obfuscate (flip (foldr f) xs) id snd (ys,[])
where
f x ((y:ys),r) = (ys,(x,y):r)
Встроенный obfuscate
zipRev xs ys = (\k c -> k (c . flip (foldr f) xs)) id snd (ys,[])
where
f x ((y:ys),r) = (ys,(x,y):r)
Бета-свертка
zipRev xs ys = (id (snd . flip (foldr f) xs)) (ys,[])
where
f x ((y:ys),r) = (ys,(x,y):r)
упрощать
zipRev xs ys = snd (foldr f (ys,[]) xs)
where
f x (y:ys,r) = (ys,(x,y):r)
Обоснование для извлечения функций, которые являются действительными fmap
из foldr
foldr (fmap . f) z [x1,x2,...,xn]
Развернуть foldr
(fmap . f) x1 . (fmap . f) x2 . ... . (fmap . f) xn $ z
Инлайн внутренний .
s
fmap (f x1) . fmap (f x2) . ... . fmap (f xn) $ z
Применить законы Функтора
fmap (f x1 . f x2 . ... . f xn) $ z
Эта-расширить раздел в скобках
fmap (\z2 -> f x1 . f x2 . ... . f xn $ z2) z
Напишите лямбда-тело в терминах foldr
fmap (\z2 -> foldr f z2 [x1,x2,...,xn]) z
Напишите лямбда-тело в терминах flip
fmap (flip (foldr f) [x1,x2,...,xn]) z