遍历Haskell中的列表拆分功能

这是我先前的question的后续行动。

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

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

我可以阅读Haskell并知道foldr是什么,但不理解此代码。您能否带我看一下这段代码,并对其进行更详细的说明?

a305069347 回答:遍历Haskell中的列表拆分功能

让我们尝试在示例输入列表上运行此功能,例如[1,2,3,4,5]

  1. 我们从foldr (\a ~(x,y) -> (a:y,x)) ([],[]) [1,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,[2]),所以(a:y,x) = ([4,2],[3,1])
  5. 输入列表的下一个元素是a = 4(x,y) = ([4,1]),所以(a:y,x) = ([5,[4,2])
  6. 没有更多的元素了,所以返回值为([5,2])

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

,

我们可以看一个例子。例如,如果我们有一个列表[1,5]。如果我们以此方式处理列表,那么我们看到foldr的计算方式为:

foldr (\a ~(x,5]

因此,a首先是列表的第一项,然后它将返回类似以下内容的内容:

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

请注意,当我们将(x,y)放在2元组的第一项之前,a元组已被交换

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

如果继续这样做,我们将获得:

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

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

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

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

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

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

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

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

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

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

(1:[2],5])
    where (x,[])

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

,

大约

foldr (\a ~(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,[b,d])

但实际上,

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

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

print . (!!1) . snd $ foldr (\a ~(x,e]
print . (!!2) . fst $ foldr (\a ~(x,e]


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

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

  • foldr ,在第二个参数中具有 lazy 的合并功能,对 corecursion 进行编码,即自上而下。首先构造结果值的头部,然后其余部分填充。在Prolog和其他地方,这很让人联想到 tail recursion modulo cons 。惰性评估作为一个概念来自“ CONS,不应评估其参数”; TRMC直到稍后才对构造函数评估 second 参数,这才是真正重要的。

,

让我们将折页翻译掉。

splatter :: [a] -> ([a],[a])
splatter = foldr (\a ~(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,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交换位置。这导致了交替模式。

,

因此,一切都发生在\a ~(x,x)函数中,其中a首先是所提供列表中的最后一项,而(x,y)是一个以{{1}开头的交替元组累加器}。 ([],[])将当前元素放在y的前面,但是元组中的a:yx列表将被交换。

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

因此,对于y的列表,请按以下步骤操作

[1,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],6] ) (2:y,x) 1 ([2,5] ) (1:y,x) [] ([1,[2,6]) no return 的操作,最好在Haskell指南的Haskell/Laziness主题中进行如下描述

  

在图案前加上波浪号会延迟对   直到组件实际使用为止。但是你运行   该值可能与模式不匹配的风险-您是在告诉   编译器“相信我,我知道它会解决的”。 (如果事实证明,   与模式不匹配,则会出现运行时错误。)   区别:

~
  

在第一个示例中,计算值是因为它必须匹配   元组模式。您评估未定义并得到未定义,这   停止诉讼。在后一个示例中,您无需打扰   评估参数直到需要它为止,结果是   永远不会,所以没有定义传递它也没关系。

,

有效地,折叠功能交替出现,从而将输入列表中的下一项添加到其中。在类似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,[]) $ [1..]
in take 5 odds

结果为[1,7,9]

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

let (odds,evens) = foldr (\a (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,x)都严格,则在达到y `k` go ys的基本情况之前,对go的求值不会终止。

使用惰性模式,该功能等效于

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

意味着我们永远不必在pfst上匹配snd;该函数现在在其第二个参数中是惰性的。这意味着

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,但是再次,我们只需要前进一个步骤即可。

本文链接:https://www.f2er.com/3051224.html

大家都在问