好的,基本上我知道选项1或2是否适用于以下情况:
naturals = 0 : map (+ 1) naturals
选项如下:
执行很糟糕,每一步都重新计算:
naturals = [0]
naturals' = 0:map (+ 1) [0] // == [0, 1]
naturals'' = 0:map (+ 1) [0, 1] // == [0, 1, 2]
naturals''' = 0:map (+ 1) [0, 1, 2] // == [0, 1, 2, 3]
naturals'''' = 0:map (+ 1) [0, 1, 2, 3] // == [0, 1, 2, 3, 4]
2。执行不是很糟糕,列表总是无限的,map
只应用一次
naturals = 0:something
|
naturals' = 0: map (+ 1) (0: something)
|
naturals'' = 0:1: map (+ 1) (0:1: something')
|
naturals''' = 0:1:2: map (+ 1) (0:1:2: something'')
|
naturals'''' = 0:1:2:3:map (+ 1) (0:1:2:3:something''')
|
表示map
执行时的位置。
我知道答案可能只是 1 或 2 ,但我也很欣赏一些关于共同递归的好解释的指示,以清除最后的疑惑:)
答案 0 :(得分:32)
执行不会像你说的那样“糟糕”。 :)懒惰的评价是你最好的朋友。懒惰是什么意思?
“事物”,这里是“尚未评估的表达”,也称为“thunks”。
以下是发生的事情:
您定义
naturals = 0 : map (+1) naturals
仅仅定义naturals
并不需要对其进行评估,因此最初naturals
只会指向未评估的表达式0 : map (+1) naturals
的thunk:
naturals = <thunk0>
在某些时候,您的程序可能会在自然模式上进行模式匹配。 (模式匹配本质上是唯一强制在Haskell程序中进行评估的东西。)也就是说,你的程序需要知道自然是空列表还是头元素后跟尾列表。这是评估定义右侧的地方,但只有在需要时才能确定naturals
或[]
是否构建了(:)
:
naturals = 0 : <thunk1>
现在,自然会指向构造函数(:)
在头元素0
上的应用,以及对于仍未评估的尾部的thunk。 (实际上,head元素仍然没有被评估,所以真正的naturals
将指向<thunk> : <thunk>
形式的某些内容,但我将把这个细节留下来。)
直到你的程序中的某个稍后点,你可以在尾部模式匹配尾部的thunk被“强制”,即被评估。这意味着要评估表达式map (+1) naturals
。评估此表达式会缩短为map
上的naturals
模式匹配:它需要知道naturals
或[]
是否构建了(:)
。
我们看到,此时,naturals
已指向(:)
的应用程序,而不是指向thunk,因此map
的此模式匹配不需要进一步评估。 map
的应用确实会立即看到naturals
足以确定它需要生成(:)
本身的应用程序,因此它会:map
生成1 : <thunk2>
map (+1) <?>
其中thunk包含1
形式的未评估表达式。 (同样,我们实际上有一个0 + 1
的thunk而不是<?>
。)naturals
指向的是什么?嗯,map
的尾部,恰好是naturals = 0 : 1 : <thunk2>
正在产生的东西。因此,我们现在有了
<thunk2>
map (+1) (1 : <thunk2>)
包含尚未评估的表达式<thunk2>
。
在你的程序中稍后,模式匹配可能会强制naturals = 0 : 1 : 2 : <thunk3>
,以便我们得到
<thunk3>
map (+1) (2 : <thunk3>)
包含尚未评估的表达式{{1}}。等等。
答案 1 :(得分:3)
我花了一段时间来弄清楚这一点,但如果你想找到(比方说)第十亿个自然数,
n = nats !! 1000000000
你在1+操作中遇到了thunk积累。我最后改写了(!!):
nth (x:xs) n = if n==0 then x else x `seq` nth xs (n-1)
我尝试了几种方法来重写nat的定义来强制每个元素,而不是写第n个,但似乎没有任何效果。
答案 2 :(得分:1)
map f xs = f (head xs) : map f (tail xs)
p0 = 0 : map (+ 1) p0
-- when p0 is pattern-matched against:
p0 = "0" :Cons: "map (+ 1) {p0}"
-- when (tail p0) is pattern-matched against:
-- {tail p0} := p1,
p1 = "(+ 1) (head {p0})" :Cons: "map (+ 1) (tail {p0})"
-- when (tail p1) is pattern-matched against:
-- {tail p1} := p2,
p2 = "(+ 1) (head {p1})" :Cons: "map (+ 1) (tail {p1})"
Haskell的列表很像Prolog的开放式列表,列表上的共同递归就像尾递归模数一样。一旦你实例化了logvar - 从某个表达式设置了一个列表的单元格值 - 它只是保持该值准备就绪,不再有对原始上下文的引用。
naturals( [A|T] ):- T=[B|R], B=A+1, naturals( T ). % "=" sic! ("thunk build-up")
为了克服Prolog的严格性,我们将来访问推动了这个过程:
naturals( nats(0) ).
next( nats(A), A, nats(B) ):-
B is A+1. % fix the evaluation to be done immediately
take( 0, Next, Z-Z, Next).
take( N, Next, [A|B]-Z, NZ):- N>0, !, next(Next,A,Next1),
N1 is N-1,
take(N1,Next1,B-Z,NZ).
Haskell毫不费力地处理了它,它的“存储”自然是懒惰的(即列表构造函数是懒惰的,并且列表构造只是通过语言的本质来“唤醒”。)
比较这些:
fix f = f (fix f)
fix f = x where x = f x -- "co-recursive" fix ?
现在,当以下使用第一个定义时,您会看到原始问题变为现实:
g = fix $ (0:) . scanl (+) 1
它的经验复杂性实际上是二次的,或更糟。但是对于第二个定义,它应该是线性的。