Как два продолжения могут компенсировать друг друга?

Я читаю некоторые хитрости для манипулирования списками, и он содержит следующее:

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