【问题标题】:Implement zip using foldr使用 foldr 实现 zip
【发布时间】:2010-09-19 02:34:23
【问题描述】:

我目前正在阅读 Real World Haskell 的第 4 章,我正在努力解决 implementing foldl in terms of 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,但我似乎没有取得任何进展。有没有可能?

【问题讨论】:

    标签: haskell functional-programming fold combinators


    【解决方案1】:
    zip2 xs ys = foldr step done xs ys
      where done ys = []
            step x zipsfn []     = []
            step x zipsfn (y:ys) = (x, y) : (zipsfn ys)
    

    这是如何工作的:(foldr step done xs) 返回一个函数,该函数使用 是的;所以我们沿着 xs 列表建立一个嵌套组合 将分别应用于 ys 的相应部分的函数。

    如何想出它:我从一般的想法开始(来自类似的 以前见过的例子),写了

    zip2 xs ys = foldr step done xs ys
    

    然后依次填写以下每一行 是使类型和值正确。这是最容易的 先考虑最简单的情况,然后再考虑困难的情况。

    第一行可以更简单地写成

    zip2 = foldr step done
    

    正如 mattiast 所展示的那样。

    【讨论】:

    • 你是邪恶的......你不是说它是 diong (foldr step done xs) 然后将其应用于 ys?
    • 这与 mattiast 的算法相同(发布快 4 秒)。对,(foldr step done xs) 返回一个消耗ys的函数;所以我们沿着 xs 列表构建一个嵌套的函数组合,每个函数都将应用于 ys 的相应部分。
    • 但是用等式来思考它更容易。我从第一行开始,然后依次填写其余的每一行,以使类型和值正确显示。
    • 您为什么不将这些解释添加到答案中并使用 where 或 let 重新格式化它,以便我接受它?
    • 好的!我对这个 stackoverflow 有点陌生。
    【解决方案2】:

    这里已经给出了答案,但不是(说明性的)推导。因此,即使经过这么多年,也许值得添加它。

    其实很简单。首先,

    折叠 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 扩展,

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

    这里很明显,如果f 在其第二个参数中是非强制的,它会在x1ys、@ 上工作首先 987654325@r1ysr1 =(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

    我们通过 调用 r1 与其他人安排信息的传递从左到右 输入列表ys1foldr f z [x2,x3,...,xn]ys1 = f x2r2ys1,作为下一步。就是这样。


    ys 短于xs(或相同长度)时,f[] case 将触发并且处理停止。但是如果ysxs 长,那么f[] 案例将不会触发,我们将进入最终的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“当有更多xs 和z = const [] 时,更多ys 会做什么”nil -foldr 中的案例,“当没有更多 xs 时,ys 做了什么”。或者f 可以自行停止,当ys 耗尽时返回[]


    注意ys 是如何被用作一种累加值的,它沿着xs 列表从左到右传递,从f 的一次调用到下一个(这里是“累加”步骤,从中剥离一个头部元素)。

    Naturally这对应于左折叠,其中一个累加步骤是“应用函数”,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
    

    等等

    【讨论】:

      【解决方案3】:

      我找到了一种与你的方法非常相似的方法:

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

      【讨论】:

        【解决方案4】:

        对于这里的非本地 Haskeller,我编写了该算法的 Scheme 版本,以便更清楚实际发生的情况:

        > (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 与给定函数的列表折叠在一起。由于惰性求值,Haskell 隐藏了内部 lambda


        进一步分解:

        输入 zip:'(1 2 3) foldr 函数被调用

        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,在本例中为(list 9 1)

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

        你还记得,f3 压缩它的论点。

        这继续等等......

        【讨论】:

          【解决方案5】:

          zip 的所有这些解决方案的问题在于它们只折叠一个列表或另一个列表,如果他们都是“好生产者”,这可能是一个问题,用列表融合的说法。您真正需要的是一个可以折叠两个列表的解决方案。幸运的是,有一篇关于这方面的论文,名为 "Coroutining Folds with Hyperfunctions"

          你需要一个辅助类型,一个超函数,它基本上是一个以另一个超函数作为参数的函数。

          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 确实称为 list fusion,这在 the GHC.Base module 中有所讨论,但可能应该更加知名。作为一个优秀的列表生成器,使用buildfoldr 可以防止大量无用的生成和立即消耗列表元素,并且可以进一步优化。

          【讨论】:

            【解决方案6】:

            我试图自己理解这个优雅的解决方案,所以我尝试自己推导类型和评估。所以,我们需要写一个函数:

            zip xs ys = foldr step done xs ys
            

            这里我们需要导出stepdone,不管它们是什么。回忆foldr 的类型,实例化为列表:

            foldr :: (a -> state -> state) -> state -> [a] -> state
            

            但是,我们的foldr 调用必须实例化为如下所示,因为我们必须接受的不是一个,而是两个列表参数:

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

            因为->right-associative,这相当于:

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

            我们的([b] -> [(a,b)])对应于原始foldr类型签名中的state类型变量,因此我们必须用它替换每个出现的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)) 构造必须创建一个递归函数,该函数将通过[4,5,6,7] 并压缩元素。 (请记住,如果原始列表之一较长,则多余的值将被丢弃)。 IOW,我们的构造必须具有 [b] -> [(a,b)] 类型。

            3 `step` done 是我们的基本情况,其中done 是一个初始值,如foldr (+) 0 [1..3] 中的0。我们不想在 3 之后压缩任何东西,因为 3 是 xs 的最终值,所以我们必须终止递归。在基本情况下如何终止对列表的递归?您返回空列表[]。但回想一下done 类型签名:

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

            因此我们不能只返回[],我们必须返回一个忽略它接收到的任何东西的函数。因此使用const:

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

            现在让我们开始弄清楚step 应该是什么。它将a 类型的值与[b] -> [(a,b)] 类型的函数组合并返回[b] -> [(a,b)] 类型的函数。

            3 `step` done 中,我们知道稍后将进入我们压缩列表的结果值必须是(3,6)(从原始xsys 得知)。因此3 `step` done 必须计算为:

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

            记住,我们必须返回一个函数,在其中我们以某种方式压缩元素,上面的代码是有意义的和类型检查的。

            现在我们假设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.:如果您受到褶皱优雅的启发,请阅读 Writing foldl using foldr,然后阅读 Graham Hutton 的 A tutorial on the universality and expressiveness of fold

            【讨论】:

              【解决方案7】:

              一个简单的方法:

              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)
              

              【讨论】:

              • 请注意,如果xsys 的长度不同,rZip 将无法正常工作。
              猜你喜欢
              • 2013-03-30
              • 1970-01-01
              • 2016-09-04
              • 2021-04-15
              • 2014-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多