【问题标题】:Walk through a list split function in Haskell浏览 Haskell 中的列表拆分功能
【发布时间】:2020-03-18 11:38:18
【问题描述】:

这是我之前的question 的后续。

我试图从here 中理解 Haskell 中的列表拆分示例:

foldr (\a ~(x,y) -> (a:y,x)) ([],[])

我可以阅读 Haskell 并且知道 foldr 是什么但不理解这段代码。你能带我看一下这段代码并详细解释一下吗?

【问题讨论】:

    标签: list haskell split fold


    【解决方案1】:

    大约,

    foldr (\a ~(x,y) -> (a:y,x)) ([],[]) [a,b,c,d,e]
    =
    let g a ~(x,y) = (a:y,x) in
    g a $ g b $ g c $ g d $ g e ([],[])
    =
    g a $ g b $ g c $ g d $ ([e],[])
    =
    g a $ g b $ g c $ ([d],[e])
    =
    g a $ g b $ ([c,e],[d])
    =
    g a $ ([b,d],[c,e])
    =
    ([a,c,e],[b,d])
    

    但真的,

    foldr (\a ~(x,y) -> (a:y,x)) ([],[]) [a,b,c,d,e]
    =
    let g a ~(x,y) = (a:y,x) in
    g a $ foldr g ([],[]) [b,c,d,e]
    =
    (a:y,x) where 
        (x,y) = foldr g ([],[]) [b,c,d,e]
    =
    (a:y,x) where 
        (x,y) = (b:y2,x2) where
                     (x2,y2) = foldr g ([],[]) [c,d,e]
    =
    (a:y,x) where 
        (x,y) = (b:y2,x2) where
                     (x2,y2) = (c:y3,x3) where
                                    (x3,y3) = (d:y4,x4) where
                                                   (x4,y4) = (e:y5,x5) where
                                                                  (x5,y5) = ([],[])
    

    通过访问(如果和何时)以自上而下的方式强制,逐渐充实,例如,

    =
    (a:x2,b:y2) where 
                     (x2,y2) = (c:y3,x3) where
                                    (x3,y3) = (d:y4,x4) where
                                                   (x4,y4) = (e:y5,x5) where
                                                                  (x5,y5) = ([],[])
    =
    (a:c:y3,b:x3) where 
                                    (x3,y3) = (d:y4,x4) where
                                                   (x4,y4) = (e:y5,x5) where
                                                                  (x5,y5) = ([],[])
    =
    (a:c:x4,b:d:y4) where 
                                                   (x4,y4) = (e:y5,x5) where
                                                                  (x5,y5) = ([],[])
    =
    (a:c:e:y5,b:d:x5) where 
                                                                  (x5,y5) = ([],[])
    =
    (a:c:e:[],b:d:[]) 
    

    但可能会以不同的顺序执行强制,具体取决于调用方式,例如

    print . (!!1) . snd $ foldr (\a ~(x,y) -> (a:y,x)) ([],[]) [a,b,c,d,e]
    print . (!!2) . fst $ foldr (\a ~(x,y) -> (a:y,x)) ([],[]) [a,b,c,d,e]
    

    等等。


    编辑: 为了解决有关惰性模式的问题,这样做是为了使结果函数具有适当的惰性:

    • foldr 与第二个参数中为 strict 的组合函数对 recursion 进行编码,即 自下而上。首先构造递归处理列表其余部分的结果,然后将结果的头部与该部分组合。

    • foldr 在第二个参数中使用 lazy 的组合函数,对 corecursion 进行编码,即 自上而下。首先构造结果值的头部,其余部分稍后填写。这很容易让人想起 Prolog 和其他地方的 尾递归模 cons。惰性评估作为一个概念来自“CONS不应该评估它的论点”; TRMC 直到稍后才评估构造函数的 second 参数,这才是真正重要的。

    【讨论】:

      【解决方案2】:

      让我们尝试在示例输入列表上运行这个函数,比如[1,2,3,4,5]

      1. 我们从foldr (\a ~(x,y) -> (a:y,x)) ([],[]) [1,2,3,4,5] 开始。这里a是列表的第一个元素,(x,y)([],[])开头,所以(a:y,x)返回([1],[])
      2. 输入列表的下一个元素是a = 2(x,y) = ([1],[]),所以(a:y,x) = ([2],[1])。请注意,列表的顺序已交换。每次迭代都会再次交换列表;但是,输入列表的下一个元素将始终添加到第一个列表中,这就是拆分的工作原理。
      3. 输入列表的下一个元素是a = 3(x,y) = ([2],[1]),所以(a:y,x) = ([3,1],[2])
      4. 输入列表的下一个元素是a = 4(x,y) = ([3,1],[2]),所以(a:y,x) = ([4,2],[3,1])
      5. 输入列表的下一个元素是a = 4(x,y) = ([4,2],[3,1]),所以(a:y,x) = ([5,3,1],[4,2])
      6. 没有剩余元素了,所以返回值为([5,3,1],[4,2])

      如演练所示,split 函数的工作原理是维护两个列表,在每次迭代时交换它们,并将输入的每个元素附加到不同的列表中。

      【讨论】:

      • 您能否也评论一下为什么在此示例中使用惰性模式匹配?
      • wiki.haskell.org/Lazy_pattern_matchsplitAt :: Int -> [a] -> ([a], [a]) 为例讨论了~(a, b)(a, b) 之间的区别。我认为相同(或至少类似)的论点将适用于split
      • 基本上,如果折叠函数使用严格的模式匹配,它必须一直递归到列表的末尾才能到达([], []),以确保有 一对要拆补丁。具有惰性模式匹配的函数等价于(\a p -> (a:fst p, snd p)),直到稍后才关心p是否是一对。
      【解决方案3】:

      有效地,折叠函数交替输入列表中的下一个项目被添加到哪个列表。像 Python 这样的语言中的类似函数是

      def split(xs):
          a0 = a = []
          b0 = b = []
          for x in xs:
              a.append(x)
              a, b = b, a
          return a0, b0
      

      使用惰性模式有两个原因:

      1. 允许立即使用结果列表,而无需等待 foldr 使用所有输入
      2. 允许拆分无限列表。

      考虑这个例子:

      let (odds, evens) = foldr (\a ~(x,y) -> (a:y,x)) ([],[]) $ [1..]
      in take 5 odds
      

      结果是[1,3,5,7,9]

      如果你放弃了惰性模式并使用了

      let (odds, evens) = foldr (\a (x,y) -> (a:y,x)) ([],[]) $ [1..]
      in take 10 odds
      

      代码永远不会终止,因为 take 在没有首先计算整个奇数列表的情况下无法获得第一个元素(更不用说前五个)了。

      这是为什么呢?考虑Data.List.foldr的定义:

      foldr k z = go
        where
          go [] = z
          go (y:ys) = y `k` go ys
      

      如果k = \a (x,y) -> (a:y, x) 在两个参数中都是严格的,那么在达到go 的基本情况之前,y `k` go ys 的评估不会终止。

      使用惰性模式,功能相当于

      \a p -> (a:snd p, fst p)
      

      意味着我们永远不必匹配 p,直到 fstsnd 这样做;该函数现在在其第二个参数中是惰性的。这意味着

      go (y:ys) = y `k` go ys
                = (\a p -> (a:snd p, fst p)) y (go ys)
                = let p = go ys in (y:snd p, fst p)
      

      立即返回而不进一步评估go。只有当我们尝试获取任一列表的第二个元素时,我们才需要再次调用go,但我们只需再前进一步。

      【讨论】:

        【解决方案4】:

        让我们把折叠移开。

        splatter :: [a] -> ([a], [a])
        splatter = foldr (\a ~(x,y) -> (a:y,x)) ([],[])
        

        这是什么意思? foldr 用于定义列表

        foldr :: (a -> r -> r) -> r -> [a] -> r
        foldr k z = go
          where
            go [] = z
            go (p : ps) = p `k` go ps
        

        让我们内联并简化:

        splatter = go
          where
            go [] = ([], [])
            go (p : ps) =
              (\a ~(x,y) -> (a:y,x)) p (go ps)
        
        splatter = go
          where
            go [] = ([], [])
            go (p : ps) =
              (\ ~(x,y) -> (p:y,x)) (go ps)
        
        splatter = go
          where
            go [] = ([], [])
            go (p : ps) =
              let (x, y) = go ps
              in (p : y, x)
        

        let 中的默认惰性模式匹配意味着在有人强制 xy 之前,我们实际上不会进行递归调用。

        要注意的关键是xy 在每个递归调用中交换位置。这导致了交替模式。

        【讨论】:

          【解决方案5】:

          所以一切都发生在\a ~(x,y) -> (a:y,x) 函数中,首先a 是所提供列表的最后一项,(x,y) 是一个以([],[]) 开头的交替元组累加器。当前元素被a:y 前置到y,但随后元组中的xy 列表被交换。

          但值得一提的是,所有新的附加都在元组的第一侧返回,这保证了第一侧最终从列表的第一项开始,因为它被附加到最后。

          所以对于[1,2,3,4,5,6] 的列表,步骤如下

          a          (x   ,   y)      return
          ----------------------------------
          6       ([]     , []     ) (6:y, x)
          5       ([6]    , []     ) (5:y, x)
          4       ([5]    , [6]    ) (4:y, x)
          3       ([4,6]  , [5]    ) (3:y, x)
          2       ([3,5]  , [4,6]  ) (2:y, x)
          1       ([2,4,6], [3,5]  ) (1:y, x)
          []      ([1,3,5], [2,4,6]) no return
          

          关于波浪号 ~ 运算符,最好在 Haskell 指南的 Haskell/Laziness 主题中描述如下

          在模式前面加上波浪号会延迟对 值直到组件部分被实际使用。但是你运行 值可能与模式不匹配的风险——你是在告诉 编译器“相信我,我知道它会解决的”。 (如果事实证明 不匹配模式,你会得到一个运行时错误。)为了说明 区别:

          Prelude> let f (x,y) = 1
          Prelude> f undefined
          *** Exception: Prelude.undefined
          
          Prelude> let f ~(x,y) = 1
          Prelude> f undefined
          1
          

          在第一个示例中,对值进行评估是因为它必须匹配 元组模式。您评估未定义并得到未定义,这 停止诉讼程序。在后一个示例中,您不必打扰 评估参数直到需要它,结果是 从来没有,所以你通过 undefined 也没关系。

          【讨论】:

          • 这并不能解释为什么有问题的代码使用惰性模式。它不是试图防止模式匹配失败。
          • @chepner 您在其中一个答案下的评论很好地解释了这一点,因此人们不应忽视这一点。然后我不能确定这是否对性能有影响,因为无论哪种方式你都必须一直处理列表,对吧..?
          • 不,我们不必一直处理所有列表。如果我们调用take 2 . fst $ foldr ...,则只会返回第一个拆分的前两个元素,即只会访问输入列表脊椎中的前 三个 位置。但是如果没有~,是的,无论如何都会访问整个列表。
          • @WillNess take 2 . fst 是一个很好的观点,但我无法确定,因为工作流程是从右到左的。它是否必须一直处理所有列表才能找到最终在元组中第一个列表的前两个位置结束的内容?
          • 没有。在“但确实”下查看我的答案。例如take 1 . fst 只减少了一次:(a:y,x) 在它之后是已知的(yx 仍然未知),这就是 take 1 完成所需要的全部。
          【解决方案6】:

          我们可以看一个例子。例如,如果我们有一个列表[1, 4, 2, 5]。如果我们这样处理列表,那么我们会看到foldr 将被计算为:

          foldr (\a ~(x,y) -> (a:y,x)) ([],[]) [1,4,2,5]

          所以这里a首先是列表的第一项,然后它会返回类似的内容:

          (1:y, x)
              where (x, y) = foldr (\a ~(x,y) -> (a:y,x)) ([],[]) [4,2,5]

          请注意,当我们将 a 添加到 2 元组的第一项时,这里的 (x, y) 元组被交换

          (1:y, x)
              where (x, y) = (4:y', x')
                    (x', y') = foldr (\a ~(x,y) -> (a:y,x)) ([],[]) [2,5]

          如果我们继续这样做,我们会得到:

          (1:y, x)
              where (x, y) = (4:y', x')
                    (x', y') = (2:y'', x'')
                    (x'', y'') = (5:y''', x''')
                    (x''', y''') = foldr (\a ~(x,y) -> (a:y,x)) ([],[]) []

          由于我们到达列表的末尾,因此我们获得了foldr … ([], []) [],2 元组([], [])

          (1:y, x)
              where (x, y) = (4:y', x')
                    (x', y') = (2:y'', x'')
                    (x'', y'') = (5:y''', x''')
                    (x''', y''') = ([],[])

          所以x''' = []y''' = [],因此解决为:

          (1:y, x)
              where (x, y) = (4:y', x')
                    (x', y') = (2:y'', x'')
                    (x'', y'') = (5:[], [])
                    (x''', y''') = ([],[])

          所以x'' = [5]y'' = []

          (1:y, x)
              where (x, y) = (4:y', x')
                    (x', y') = (2:[], [5])
                    (x'', y'') = (5:[], [])
                    (x''', y''') = ([],[])

          所以x' = [5]y' = [2]

          (1:y, x)
              where (x, y) = (4:[5], [2])
                    (x', y') = (2:[], [5])
                    (x'', y'') = (5:[], [])
                    (x''', y''') = ([],[])

          所以x = [4, 5]y = [2] 所以最终我们得到:

          (1:[2], [4,5])
              where (x, y) = (4:[5], [2])
                    (x', y') = (2:[], [5])
                    (x'', y'') = (5:[], [])
                    (x''', y''') = ([],[])

          所以结果是预期的([1,2], [4,5])

          【讨论】:

          • 我想结果应该是([1,2],[4,5])
          • 谢谢。还有一个问题:此代码是否适用于 foldl 而不是 foldr ?为什么?
          • @Michael:项目将按相反的顺序排列,此外,根据函数的长度,列表的第一项将在 2 元组的第一项或第二项中。跨度>
          • 知道了。谢谢。
          猜你喜欢
          • 2020-07-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-07-20
          • 2019-06-05
          相关资源
          最近更新 更多