相互递归 - 有人可以帮助解释这段代码是如何工作的吗?

时间:2008-12-29 08:44:49

标签: haskell recursion

我正在阅读“对Haskell的一个温和的介绍”,并且在早期它使用了这个例子,它在GHC中运行良好,在我的大脑中可怕:

initial                 = 0
next resp               = resp
process req             = req+1

reqs                    = client initial resps
resps                   = server reqs

server          (req:reqs)   = process req : server reqs
client initial ~(resp:resps) = initial : client (next resp) resps

和主叫代码:
take 10 reqs

我是如何看到它的,reqs被调用,通过args 0和client调用resps。因此,现在不需要调用resps ......然后再调用reqs?这一切似乎都是如此无限......如果有人能够详细说明它是如何运作的,我会非常感激!

5 个答案:

答案 0 :(得分:12)

我发现通常手动计算小Haskell程序的行为是值得的。评估规则非常简单。要记住的关键是Haskell是非严格的(又名 lazy ):表达式仅在需要时进行评估。懒惰是看似无限定义可以产生有用结果的原因。在这种情况下,使用take意味着我们只需要无限列表reqs的前10个元素:它们都是我们“需要”的。

实际上,“需要”通常由模式匹配驱动。例如,通常会对列表表达式进行评估,直到我们可以在函数应用之前区分[](x:xs)。 (请注意,模式前面的'~'与client的定义一样,使其变得懒惰(或无可辩驳):懒惰模式不会强制其参数直到整个表达被迫。)

记住take是:

take 0 _      = []
take n (x:xs) = x : take (n-1) xs

take 10 reqs的评估如下:

take 10 reqs 
      -- definition of reqs
    = take 10 (client initial resps)
      -- definition of client [Note: the pattern match is lazy]
    = take 10 (initial : (\ resp:resps' -> client (next resp) resps') 
                             resps)
      -- definition of take
    = initial : take 9 ((\ resp:resps' -> client (next resp) resps') 
                            resps)
      -- definition of initial
    = 0 : take 9 ((\ resp:resps' -> client (next resp) resps') 
                      resps)
      -- definition of resps
    = 0 : take 9 ((\ resp:resps' -> client (next resp) resps') 
                      (server reqs))
      -- definition of reqs
    = 0 : take 9 ((\ resp:resps' -> client (next resp) resps') 
                      (server (client initial resps)))
      -- definition of client
    = 0 : take 9 ((\ resp:resps' -> client (next resp) resps') 
                      (server (initial : {- elided... -}))
      -- definition of server
    = 0 : take 9 ((\ resp:resps' -> client (next resp) resps') 
                      (process initial : server {-...-}))
      -- beta reduction 
    = 0 : take 9 (client (next (process initial)) (server {-...-})
      -- definition of client 
    = 0 : take 9 (next (process initial) : {-...-})
      -- definition of take 
    = 0 : next (process initial) : take 8 {-...-}
      -- definition of next 
    = 0 : process initial : take 8 {-...-}
      -- definition of process 
    = 0 : initial+1 : take 8 {-...-}
      -- definition of initial 
    = 0 : 1 : take 8 {-...-}
      -- and so on...

答案 1 :(得分:11)

理解此代码需要两项技能:

  • 区分'定义',它可能是无限的(如自然数集naturals = (1 : map '\n->n+1' naturals),或处理请求列表)和'减少',这是将实际数据映射到这些数据的过程定义
  • 看到这个客户端 - 服务器应用程序的结构:它只是一对与彼此交谈的进程:'client-server'是一个坏名字,真的:它应该被称为'wallace-gromit'或'foo-bar ',或者说哲学家或其他什么,但它是对称的:两方都是同行。

正如Jon已经说过的那样,减少是以懒惰的方式工作(又称“按需要呼叫”):take 2 naturals不会首先评估完整的自然组合,而只是选择第一个自然组合,将其添加到take 1 (map '\n->n+1' naturals),这将减少到[1,(1 + 1)] = [1,2]。

现在客户端服务器应用程序的结构就是这个(我的眼睛):

  • server是一种使用process函数
  • 创建请求列表中的响应列表的方法
  • client是一种基于响应创建请求的方法,并将该请求的响应附加到响应列表。

如果你仔细观察,你会发现两者都是“从y:ys创建x:xs的方法”。我们可以统一称呼他们wallacegromit

现在很容易理解,如果仅使用响应列表调用client

someresponses = wallace 0 [1,8,9]    -- would reduce to 0,1,8,9
tworesponses  = take 2 someresponses -- [0,1]

如果答案不是字面上知道的,而是由gromit生成的,我们可以说

gromitsfirstgrunt = 0
otherresponses = wallace gromitsfirstgrunt (gromit otherresponses)
twootherresponses = take 2 otherresponses -- reduces to [0, take 1 (wallace (gromit ( (next 0):...) )]
                                          -- reduces to [0, take 1 (wallace (gromit ( 0:... ) )  ) ]
                                          -- reduces to [0, take 1 (wallace (1: gromit (...)  )  ) ]
                                          -- reduces to [0, take 1 (1 : wallace (gromit (...)  ) ) ]
                                          -- reduces to [0, 1 ]

其中一个同行需要“开始”讨论,因此提供给wallace的初始值。

还要注意gromit模式之前的〜:这告诉Haskell列表参数的内容不需要减少 - 如果它看到它是一个列表,那就足够了。在wikibook on Haskell中有一个很好的主题(寻找“懒惰模式匹配”)。

答案 2 :(得分:4)

我玩Haskell已经有一段时间了,但我很确定它是懒惰评估的,这意味着它只计算它实际需要的东西。因此,虽然reqs是无限递归的,但是take 10 reqs只需要返回列表的前10个元素,这就是实际计算的所有元素。

答案 3 :(得分:2)

另见我对“绑结”问题的回答here

答案 4 :(得分:0)

看起来很好的混淆。如果您准确地阅读,您会发现它很简单:

下一个?它的身份

服务器?它只是地图过程,即地图'\ n-> n + 1'

客户端?如何编写0:服务器客户端,这是不明智的方式,例如0:地图'\ n-> n + 1'[0:地图'\ n-> n + 1'[0:...]]