Haskell 如何处理协同递归?

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,

好的,基本上我不知道选项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]
二,。执行并不糟糕,列表总是无限的,并且
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