【问题标题】:How is co-recursion handled?如何处理协同递归?
【发布时间】:2012-05-02 16:39:30
【问题描述】:

好的,基本上我不知道选项 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, 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 在其执行中的位置。

我确实知道答案可能只有 12 但我也希望能提供一些关于共同递归的良好解释的指针,以消除最后的疑虑 :)

【问题讨论】:

    标签: haskell recursion


    【解决方案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 的开放式列表,并且列表上的共同递归类似于尾递归模 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
    

    它的经验复杂度实际上是二次的,或者更糟。但对于第二个定义,它应该是线性的。

    【讨论】:

    • 我忘了说谢谢,但这是一个有趣的平行。在你指出之前我没有意识到它实际上是尾递归模缺点:)
    • 我认为是。它也就像 Python 的生成器一样。我想。 :)
    • ...还有累加器参数技术:fac n = let { go a k | k==n = a; go a k = go (a*(k+1)) (k+1)} in go 1 0 = (!! n) $ scanl (*) 1 [1..] = (!! n) (let xs = zipWith (*) (1:xs) [1..] in (1:xs))——它是关于“向上计数”——而递归是关于“向下计数”。我想。 :)
    • 当然还有差异列表或不完整的结构(有孔等)。
    【解决方案2】:

    我花了一段时间才弄明白,但如果你想找到(比如说)第 10 亿个自然数,

    n = nats !! 1000000000
    

    您在 1+ 操作中遇到了 thunk 累积。我最终重写了(!!):

    nth (x:xs) n = if n==0 then x else x `seq` nth xs (n-1)
    

    我尝试了几种方法来重写 nats 的定义以强制每个元素,而不是写 nth,但似乎没有任何效果。

    【讨论】:

    • 如果你想从naturals做,你必须将每个元素的求值绑定到对应的(:)构造函数,这是map做不到的。但是,您可以使用 foldr 进行操作,例如nats = 0 : foldr f [] nats where f x ys = let y = x+1 in y `seq` (y : ys)
    • 跨过更大的块可能会更快:nnth xs k n | k>n = head xs`seq`(xs!!n) | otherwise = head xs`seq`(case drop k xs of [] -> xs!!n ; ys -> nnth ys k (n-k))。数组上下文中的相关代码是stackoverflow.com/a/10238950/849891
    【解决方案3】:

    执行不会像您所说的那样“糟糕”。 :) 懒惰的评价是你最好的朋友。懒惰是什么意思?

    1. 在真正需要结果之前不会对事物进行评估;
    2. 事物最多评估一次。

    这里的“事物”是“尚未评估的表达式”,也称为“thunk”。

    会发生什么:

    你定义

    naturals = 0 : map (+1) naturals
    

    仅仅定义naturals 并不需要对其求值,所以最初naturals 将只指向未求值表达式0 : map (+1) naturals 的thunk:

    naturals = <thunk0>
    

    在某些时候,您的程序可能会在自然值上进行模式匹配。 (模式匹配本质上是在 Haskell 程序中强制求值的唯一因素。)也就是说,您的程序需要知道 naturals 是空列表还是后跟一个尾列表的头元素。这是您定义的右侧将被评估的地方,但仅限于确定naturals 是由[] 还是(:) 构造的:

    naturals = 0 : <thunk1>
    

    That is naturals 现在将指向构造函数 (:) 对 head 元素 0 的应用,并为仍未求值的尾部添加 thunk。 (实际上,head 元素还没有被计算,所以naturals 将指向&lt;thunk&gt; : &lt;thunk&gt; 形式的东西,但我将省略这个细节。)

    直到您的程序中稍后的某个时间点,您可以在尾部进行模式匹配时,尾部的 thunk 才被“强制”,即评估。这意味着要计算表达式map (+1) naturals。评估这个表达式简化为naturals上的map模式匹配:它需要知道naturals是由[]还是(:)构造的。 我们看到,此时naturals 已经指向(:) 的应用程序,而不是指向一个thunk,所以map 的这种模式匹配不需要进一步评估。 map 的应用程序确实立即看到足够的 naturals 来确定它需要生成 (:) 本身的应用程序,因此它确实:map 生成 1 : &lt;thunk2&gt; 其中 thunk 包含未评估的表达式形成map (+1) &lt;?&gt;。 (同样,我们实际上有一个0 + 1 的thunk 而不是1。)&lt;?&gt; 指向什么?好吧,naturals 的尾巴,恰好是 map 正在生产的东西。因此,我们现在有

    naturals = 0 : 1 : <thunk2>
    

    &lt;thunk2&gt; 包含尚未计算的表达式 map (+1) (1 : &lt;thunk2&gt;)

    在您程序的稍后时间,模式匹配可能会强制&lt;thunk2&gt;,因此我们得到

    naturals = 0 : 1 : 2 : <thunk3>
    

    &lt;thunk3&gt; 包含尚未计算的表达式 map (+1) (2 : &lt;thunk3&gt;)。以此类推。

    【讨论】:

    • 所以每个元素都将是由一堆添加组成的更大的重击?
    • @TikhonJelvis 是的,除非程序当然强制对元素进行评估。这就是使惰性评估程序的空间复杂性有时难以推理的原因:即使是经验丰富的 Haskell 程序员有时也会对看似天真的程序的内存需求感到惊讶。这就是我们有seq 之类的原因。
    猜你喜欢
    • 2014-05-23
    • 1970-01-01
    • 2014-09-26
    • 2017-03-26
    • 1970-01-01
    • 1970-01-01
    • 2012-04-11
    • 2015-05-20
    • 2014-09-13
    相关资源
    最近更新 更多