【发布时间】:2020-03-18 11:38:18
【问题描述】:
这是我之前的question 的后续。
我试图从here 中理解 Haskell 中的列表拆分示例:
foldr (\a ~(x,y) -> (a:y,x)) ([],[])
我可以阅读 Haskell 并且知道 foldr 是什么但不理解这段代码。你能带我看一下这段代码并详细解释一下吗?
【问题讨论】:
这是我之前的question 的后续。
我试图从here 中理解 Haskell 中的列表拆分示例:
foldr (\a ~(x,y) -> (a:y,x)) ([],[])
我可以阅读 Haskell 并且知道 foldr 是什么但不理解这段代码。你能带我看一下这段代码并详细解释一下吗?
【问题讨论】:
大约,
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 参数,这才是真正重要的。
【讨论】:
让我们尝试在示例输入列表上运行这个函数,比如[1,2,3,4,5]:
foldr (\a ~(x,y) -> (a:y,x)) ([],[]) [1,2,3,4,5] 开始。这里a是列表的第一个元素,(x,y)以([],[])开头,所以(a:y,x)返回([1],[])。a = 2和(x,y) = ([1],[]),所以(a:y,x) = ([2],[1])。请注意,列表的顺序已交换。每次迭代都会再次交换列表;但是,输入列表的下一个元素将始终添加到第一个列表中,这就是拆分的工作原理。a = 3和(x,y) = ([2],[1]),所以(a:y,x) = ([3,1],[2])。a = 4和(x,y) = ([3,1],[2]),所以(a:y,x) = ([4,2],[3,1])。a = 4和(x,y) = ([4,2],[3,1]),所以(a:y,x) = ([5,3,1],[4,2])。([5,3,1],[4,2])。如演练所示,split 函数的工作原理是维护两个列表,在每次迭代时交换它们,并将输入的每个元素附加到不同的列表中。
【讨论】:
splitAt :: Int -> [a] -> ([a], [a]) 为例讨论了~(a, b) 和(a, b) 之间的区别。我认为相同(或至少类似)的论点将适用于split。
([], []),以确保有是 一对要拆补丁。具有惰性模式匹配的函数等价于(\a p -> (a:fst p, snd p)),直到稍后才关心p是否是一对。
有效地,折叠函数交替输入列表中的下一个项目被添加到哪个列表。像 Python 这样的语言中的类似函数是
def split(xs):
a0 = a = []
b0 = b = []
for x in xs:
a.append(x)
a, b = b, a
return a0, b0
使用惰性模式有两个原因:
foldr 使用所有输入考虑这个例子:
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,直到 fst 或 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,但我们只需再前进一步。
【讨论】:
让我们把折叠移开。
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 中的默认惰性模式匹配意味着在有人强制 x 或 y 之前,我们实际上不会进行递归调用。
要注意的关键是x 和y 在每个递归调用中交换位置。这导致了交替模式。
【讨论】:
所以一切都发生在\a ~(x,y) -> (a:y,x) 函数中,首先a 是所提供列表的最后一项,(x,y) 是一个以([],[]) 开头的交替元组累加器。当前元素被a:y 前置到y,但随后元组中的x 和y 列表被交换。
但值得一提的是,所有新的附加都在元组的第一侧返回,这保证了第一侧最终从列表的第一项开始,因为它被附加到最后。
所以对于[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 也没关系。
【讨论】:
take 2 . fst $ foldr ...,则只会返回第一个拆分的前两个元素,即只会访问输入列表脊椎中的前 三个 位置。但是如果没有~,是的,无论如何都会访问整个列表。
take 2 . fst 是一个很好的观点,但我无法确定,因为工作流程是从右到左的。它是否必须一直处理所有列表才能找到最终在元组中第一个列表的前两个位置结束的内容?
take 1 . fst 只减少了一次:(a:y,x) 在它之后是已知的(y,x 仍然未知),这就是 take 1 完成所需要的全部。
我们可以看一个例子。例如,如果我们有一个列表[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 ?为什么?