两个延续如何相互抵消?

时间:2019-05-14 01:59:43

标签: haskell fold continuations continuation-passing

我正在阅读Some Tricks for List Manipulation,其中包含以下内容:

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)

我不明白您如何“取消”堆叠的延续,以从顶部的代码块到达底部的代码块。您希望通过哪种模式进行转换,为什么会起作用?

5 个答案:

答案 0 :(得分:16)

函数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 id === id。这意味着您可以经常使用此转换来解开构成obfuscate d函数的双连续计算,方法是将obfuscate从合成中分解出来。这个问题就是这种纠结的一个例子。

顶部代码块中的f是底部块obfuscate的{​​{1}} d版本(更确切地说,顶部f是{{1} } d版本的底部f x)。您可以注意到顶部obfuscate如何将外部延续应用到转换其输入的函数,然后将整个事物应用于内部延续,就像在f x主体中一样。

所以我们可以开始解开f

obfuscate

由于zipRev的作用是彼此组成一堆zipRev xs ys = foldr f id xs snd (ys,[]) where f x = obfuscate (\(y:ys,r) -> (ys,(x,y):r)) d函数(并将其全部应用到foldr,我们可以在右边保留),我们可以将obfuscate分解到整个折叠的外部:

id

现在应用obfuscate的定义并简化:

zipRev xs ys = obfuscate (\accum -> foldr f accum xs) id snd (ys,[])
  where
    f x (y:ys,r) = (ys,(x,y):r)

QED!

答案 1 :(得分:12)

提供功能

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

答案 2 :(得分:2)

让我们尝试从基本的角度理解此代码。一个人奇怪,它甚至做什么?

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)

在这里,我们使用lambda lifting恢复了g combinator

因此,由于f x k = k . g xkx的左侧,因此输入列表被转换为反向组成链,

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列表上为左(即自下而上)。直接将其编码为带有严格的reducer的右折,因此流的确在xs上从右到左。链中最底端的动作(snd)最后执行,因此在新代码中,它成为最顶端的操作(仍最后执行):

zipRev xs ys = snd (foldr h (ys,[]) xs)
  where
        h x        (y:ys, r) =   (ys, (x,y):r)

g x c被用作原始代码的延续,c被用作第二层延续;但实际上一直以来都是从右边开始的常规折叠。


因此,确实可以将反向的第一个列表与第二个列表压缩在一起。这也不安全;它错过了一个子句:

        g x  c     ([],   r) = c ([], r)        -- or just `r`
        g x  c     (y:ys, r) = c (ys, (x,y):r)

(更新:) duplode(和Joseph Sible)的答案使lambda的举升方式有所不同,从而更适合该任务。它是这样的:

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      ]

简单。 :)翻转两次根本不翻转。

答案 3 :(得分:2)

让我们先进行一些外观调整:

-- 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 . g x)传递给另一个函数(k,“双重延续”,as user11228628 puts it)。

尽管我们可以合理地预期,f的重复使用会随着折叠的进行而以某种方式构成由列表元素构成的g x内态,但是内态的构成顺序可能不会立即变得显而易见,因此我们最好走几步,以确保:

-- 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的{​​{1}}自变量所做的所有工作都是使用内同态对它进行后期组合,因此我们不妨一味地将其分解:

foldr f id xs

请注意我们是如何从zipRev xs ys = (snd . foldr h id xs) (ys,[]) where h x e = g x . e 转到c . g x的。可以将其描述为原始实施中CPS欺骗的附带影响。

最后一步是注意g x . e与我们对implement foldr in terms of foldMap for the Endo monoid所做的精确对应。或者,更明确地说:

h x e = g x . e

这最终使我们找到了想要的东西:

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

答案 4 :(得分:2)

user11228628's answer使我理解。这是我阅读本书时的一些见解,以及一些逐步的转换。


见解

  • 续集不会直接取消。最终它们只能被取消(通过降低beta值),因为可以将它们排除在外。
  • 您要进行此转换的模式是\k c -> k (c . f)的{​​{1}}(或者,如果您喜欢不可读的无指向的(. (. f)))(请注意,f不是lambda的参数。
  • duplode points out in a comment一样,可以将连续传递样式函数视为函子,f是它们对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

Beta减少

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 内联

.

应用仿函数定律

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

编写lambda正文
foldr

根据fmap (\z2 -> foldr f z2 [x1,x2,...,xn]) z

编写lambda正文
flip

奖金:从fmap (flip (foldr f) [x1,x2,...,xn]) z 中提取有效contramap的函数的理由

foldr

展开foldr (contramap . f) z [x1,x2,...,xn]

foldr

内联(contramap . f) x1 . (contramap . f) x2 . ... . (contramap . f) xn $ z 内联

.

应用逆定律

contramap (f x1) . contramap (f x2) . ... . contramap (f xn) $ z

使用扩展括号中的部分

contramap (f xn . ... . f x2 . f x1) $ z

根据contramap (\z2 -> f xn . ... . f x2 . f x1 $ z2) z

编写lambda正文
foldr

根据contramap (\z2 -> foldr f z2 [xn,...,x2,x1]) z

编写lambda正文
flip

应用contramap (flip (foldr f) [xn,...,x2,x1]) z = foldr f z (reverse xs)

foldl (flip f) z xs