Haskell 如何处理协同递归?
好的,基本上我不知道选项1或2是否适用于以下情况:Haskell 如何处理协同递归?,haskell,recursion,Haskell,Recursion,好的,基本上我不知道选项1或2是否适用于以下情况: naturals = 0 : map (+ 1) naturals 其中选项为: 1.执行非常糟糕,每一步都会重新计算: naturals = [0] naturals' = 0:map (+ 1) [0] // == [0, 1] naturals'' = 0:map (+ 1) [0, 1] // == [0, 1, 2] naturals''' = 0:map (+ 1) [0, 1,
naturals = 0 : map (+ 1) naturals
其中选项为:1.执行非常糟糕,每一步都会重新计算:
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]
二,。执行并不糟糕,列表总是无限的,并且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''')
使用|
指示执行映射时的位置
我知道答案可能只有1或2,但我也希望有一些关于协递归的好解释来澄清最后的疑问:)执行不会像你说的那样“糟糕”。)懒惰的评价是你在这里最好的朋友。懒惰是什么意思
在真正需要结果之前,没有对事物进行评估李>
事情最多只能评估一次
这里的“事物”是“尚未计算的表达式”,也称为“thunks”
发生的情况如下:
你定义
naturals = 0 : map (+1) naturals
仅仅定义naturals
并不需要对其求值,因此最初naturals
只会指向未求值表达式0:map(+1)naturals
:
naturals = <thunk0>
也就是说,naturals现在将指向head元素0
上构造函数(:)
的应用程序,并指向仍然未计算的尾部。(实际上,head元素仍然没有赋值,因此真正的naturals
将指向某种形式的:
,但我将省略该细节。)
直到程序中稍后的某个点,在该点上,您可能会在尾部进行模式匹配,才可以“强制”对尾部进行重击,即进行评估。这意味着要计算表达式map(+1)naturals
。对该表达式的求值简化为map
在naturals
上的模式匹配:它需要知道naturals
是由[]
还是(:)
构造的。
我们看到,在这一点上,naturals
并没有指向thunk,而是已经指向了(:)
的应用程序,因此,map
的模式匹配不需要进一步评估。map
的应用程序确实立即看到了足够多的自然
,以确定它需要生成(:)
本身的应用程序,因此:map
生成1:
,其中thunk包含表单map(+1)
的未赋值表达式。(同样地,我们没有使用1
,而是使用了0+1
)什么是
?嗯,naturals
的尾部,恰好是map
产生的。因此,我们现在有
naturals = 0 : 1 : <thunk2>
naturals=0:1:
包含尚未计算表达式的映射(+1)(1:)
在程序的稍后阶段,模式匹配可能会强制执行
,以便
naturals = 0 : 1 : 2 : <thunk3>
naturals=0:1:2:
包含尚未计算表达式的映射(+1)(2:)
。等等。我花了一段时间才弄明白,但如果你想找到(比如)第十亿个自然数
n = nats !! 1000000000
你在1+操作中碰到了一个重击累积。我最终改写了(!!):
我尝试了几种重写NAT定义的方法来强制每个元素,而不是写N,但似乎没有任何效果
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的开放式列表,列表上的共同递归类似于模cons的尾部递归。一旦您实例化了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
它的经验复杂性实际上是二次的,甚至更糟。但是对于第二个定义,它是线性的,正如它应该的那样。那么每个元素都将是由一堆附加元素组成的一个更大的thunk?@TikhonJelvis是的,当然,除非程序强制对元素进行评估。这就是为什么懒散评估程序的空间复杂性有时难以解释的原因:即使是Haskell程序员的经验有时也会对看似无辜的程序的内存需求感到惊讶。这就是为什么我们有seq
等等。如果你想从naturals
进行计算,你必须将每个元素的求值与相应的(:)
构造函数联系起来,这是map
无法做到的。但是,您可以使用foldr
来实现这一点,例如nats=0:foldr f[]nats,其中fx ys=let y=x+1在y`seq`(y:ys)
跨过更大的块可能会更快:nnth xs k n | k>n=head xs`seq`(xs!!n)|否则=head xs seq`(case drop k xs of[]->xs n;ys nnth xs k(n-k)
。数组上下文中的一个相关代码是。我忘了说谢谢,但这是一个有趣的并行代码。在你们指出它之前,我不知道它实际上是尾递归模cons:)我想是的。它也和Python的生成器一样。我想。。。还有累加器参数技巧:fac n=let{go a k | k==n=a;go a k=go(a*(k+1))(k+1)}在go 1中0=(!!n)$scanl(*)1[1..]=(!!n)(let xs=zipWith(*)(1:xs)[1..]在(1:xs中)
--它是关于“计数”,递归是关于“计数”。我想……)当然,还有差异列表,或者不完整的结构(带有孔等)。
fix f = f (fix f)
fix f = x where x = f x -- "co-recursive" fix ?
g = fix $ (0:) . scanl (+) 1