函数式语言向语言和运行时让渡控制权的途径——递归

递归是“以一种自相似的方式来重复事物的过程”,也是向运行时托付操作细节的一个例子,而且和函数式编程有着极为密切的联系。以具体的实践来说,递归是以一种带点计算机科学味道的方式来对一组事物进行迭代,让事物的集合反复对自身调用同样的方法,使集合随着每次迭代不断缩小,同时要始终小心地保证退出条件的有效性。

问题核心就是对一个不断变短的列表反复地做同一件事,把递归用在这样的场合,写出来的代码就容易理解。

换个角度看列表

在C或类C语言(包括Java)角度,列表是一个带索引的集合。
依靠(不一定直接露面的)索引来完成的列表遍历:

Groovy还提供了eachWithIndex()迭代子,要求传给它的代码块带有索引参数,适用于需要显式访问索引的场合。
作为“带索引的格子”的列表形象:

在函数式语言角度,列表是由列表的第一个元素(叫作头部)和列表的其余元素(叫作尾部)这两部分组合而成。
分成头和尾两部分的列表形象:

把列表想象成头部和尾部的组合,有利于使用递归的方式来组织迭代:
以递归方式进行的列表遍历:

recurseList()方法首先检查传入的列表里还有没有元素。如果没有,那就表示迭代工作已经完成,可以返回了。如果还有元素,那么用Groovy提供的head()方法取出第一个元素,把它打印出来,然后继续对列表的余下部分调用recurseList()方法。

递归方法的缺点

递归操作往往受制平台而存在一些固有的技术限制,因此这种技法绝非万灵药。但对于长度不大的列表来说,递归操作是安全的。

从长远来看,还是应该更多地投入到良好的代码结构上,技术限制总会随着时间减少或者消失化中。递归写法作为一种有缺点的代码结构,其优点并不那么直观。

命令式和递归式的对比

命令式写法的筛选函数,Groovy实现:


先创建一个新列表来存放希望保留的元素,然后对原列表进行迭代,让谓词判定每个元素的去留,最后返回保留元素的列表。

递归式写法的筛选函数,Groovy实现:


filter() 函数首先检查传入的列表的大小,若列表中已经没有元素,则返回列表。否则用筛选条件检查列表的头部,如果头部满足筛选条件,就把它放入列表(代码中用了一个空列表“[]”作为初始值来保证返回类型是正确的),不然就继续递归地对列表的尾部筛选下去。

谁来管理状态?在命令式的写法中,是“我”在管理状态。“我”必须创建一个叫new_list的新变量,“我”负责向新列表添加元素,“我”负责在筛选完成后返回新列表。而在递归写法中,是语言在管理返回值,它从递归栈里收集每次方法调用的返回结果,构造出最终的返回值。递归例子中的filter()方法的每一条结束路径都返回到递归调用的上一层,随着栈中的调用一层一层地返回,各层得到的中间结果也自动汇集到一起。于是程序员卸下了对new_list的管理责任,交由语言去替我们照料。
利用递归,把状态的管理责任推给运行时。

Scala语言递归的实现(还含柯里化)


Scala的列表构造运算符“::”起到了提高代码可读性的作用,筛选通过和不通过这两种情形下返回结果的变动,表述得清晰易懂。filter()方法递归地使用参数p来筛选一个整数列表,其中参数p是一个布尔函数,或者按照函数式领域的术语叫作“谓词”(predicate)函数。filter()方法检查列表是否为空,若是则直接返回;否则用谓词来检验列表的第一个元素(xs.head),判断是否应放入筛选后的列表。

如果头部满足谓词条件,那么就返回以该头部为首,再加上尾部的筛选结果组成的新列表。如果头部通不过谓词的检验,返回的就只有列表余下部分的筛选结果。

递归对开发者的解放效果或许不像垃圾收集那么显著,不过它切实地揭示了编程语言的一个重要的发展方向:通过移交“不确定因素”的控制权给运行时来消解它们。如果程序员不准插手列表操作的中间结果,那么就不会引入那些在交互中产生的错误。

尾调用优化

递归没有成为一种平常的操作,其中一个主要原因是栈的增长。递归操作一般的实现方式,都是把中间结果放在栈里,于是没有为递归专门优化的语言就会遇到栈溢出的问题。

而像Scala、Clojure这些语言则各自采用了不同的方式来规避这方面的局限。开发者也可以使用尾调用优化(tail-call optimization)的写法来帮助运行时克服栈的增长问题。当递归调用是函数执行的最后一个调用的时候,运行时往往可以在栈里就地更新,而不需要增加新的栈空间。

很多函数式语言实现了没有栈增长的尾递归。Erlang用尾递归来实现长时间运行的进程,相当于运行在应用里面的一系列微服务,它们从别的进程接收消息,并按照消息中的要求来代表别的进程执行任务。这些接收消息并受消息左右的尾递归循环还有调整微服务内部状态的能力,因为对不可变的当前状态的任何作用结果,都可以放在表示新状态的变量里传入下一轮递归而生效。

递归在开发中用得不多的原因

应该部分地归咎于大多数命令式语言呆滞的语法配合,让一件不太容易的事情变得难上加难。函数式语言的简洁语法和灵活配合,才使递归成为简单可行的代码重用选项之一。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容