函数式编程 – 很多重点recursion,为什么?

我正在介绍函数式编程[FP](使用Scala)。 从我最初的学习中得出的一件事是FPs很大程度上依赖于recursion。 而且在 FP中,似乎只是通过编写recursion函数来完成迭代。

而且由于recursion的使用量很大,FPs不得不担心StackoverflowExceptions通常是由于长时间recursion调用。 通过引入一些优化来解决这个问题(在维护堆栈框中进行与尾部recursion相关的优化以及从Scala v2.8开始的@tailrec批注)

有人可以请启发为什么recursion对函数式编程范例如此重要吗? 函数式编程语言的规范中是否存在某些东西,如果我们迭代地执行某些东西,会被“违反”? 如果是的话,我也很想知道这一点。

PS:请注意,我是function性编程的新手,所以请随时指出我现有的资源,如果他们解释/回答我的问题。 我也明白,斯卡拉特别是提供了支持做迭代的东西。

教会图灵论文强调了不同的可计算性模型之间的等价性。

使用recursion,我们不需要一个可变的状态,而解决一些问题,这使得可以用更简单的术语来指定语义。 因此,解决scheme在forms上可以更简单。

我认为Prolog比函数式语言更好地展示了recursion的有效性(它没有迭代),以及我们在使用它时遇到的实际限制。

纯function编程意味着编程没有副作用。 也就是说,如果你写了一个循环,你的循环体就不会产生副作用。 因此,如果你想让你的循环做一些事情,它必须重用先前迭代的结果,并为下一次迭代产生一些东西。 因此,你的循环的主体是一个函数,将前一次执行的结果作为参数,并用自己的结果调用下一次迭代。 这与直接为循环编写recursion函数没有太大的优势。

一个不做小事的程序将不得不在某些事情上迭代一些东西。 对于函数式编程,这意味着程序必须使用recursion函数。

带来要求recursion执行的特性是不变的variables。

考虑一个计算列表总和的简单函数(伪代码):

 fun calculateSum(list): sum = 0 for each element in list: # dubious sum = sum + element # impossible! return sum 

现在,每个迭代列表中的element都是不同的,但是我们可以重写这个以使用具有lambda参数的foreach函数来摆脱这个问题:

 fun calculateSum(list): sum = 0 foreach(list, lambda element: sum = sum + element # impossible! ) return sum 

不过,在lambda的每次运行中sumvariables的值必须被改变。 这是一个不可变variables的语言是非法的,所以你必须以不改变状态的方式重写它:

 fun calculateSum([H|T]): return H + calculateSum(T) fun calculateSum([]): return 0 

现在,这个实现将需要大量的调用堆栈,并且所有小操作都会执行的程序不能很快运行。 因此,我们把它重写为尾recursion,所以编译器可以做尾调用优化:

 fun calculateSum([H|T], partialSum): return calculateSum(T, H + partialSum) fun calculateSum([], partialSum): return partialSum fun calculateSum(list): return calculateSum(list, 0) 

当然,如果你想无限循环,你绝对需要一个尾recursion调用,否则它会堆栈溢出。

Scala中的@tailrec注释是帮助您分析哪些函数是尾recursion的工具。 你声称“这个函数是尾recursion的”,然后编译器会告诉你,如果你错了。 在Scala中,与其他函数式语言相比,这一点尤为重要,因为它所运行的机器JVM不能很好地支持尾部调用优化,所以在所有相同的情况下,您将无法获得Scala中的尾部调用优化它在其他function语言。

TL; DR :recursion用于处理无处不在的归纳定义的数据

当你在更高的抽象层次上进行操作时,recursion是很自然的。 函数式编程不仅仅是用函数编码, 它是关于在更高的抽象层次上进行操作,在那里自然使用函数。 使用函数,重用相同的函数(再次调用它)是自然的,从任何有意义的上下文中。

世界是通过重复类似/相同的构build块而build立的。 如果你把一块布料切成两块,就有两块布料。 math归纳是math的核心。 我们,人类,(如1,2,3 )。 任何归纳定义的事物 (例如, 从{1开始的{数字}是{1从2开始的数字} )根据同样的事物被定义/构造的情况,通过recursion函数来处理/分析是自然的。

recursion无处不在。 任何迭代循环无论如何都是伪装的recursion,因为当你重新进入循环时,你再次重新进入同一个循环(只是可能有不同的循环variables)。 所以它不像发明新的计算概念,更像是发现基础,并且明确expression


所以,recursion是很自然的。 我们只是写下关于我们问题的一些规律,一些方程涉及我们正在定义的函数,它保留了一些不variables(在函数被连贯定义的假设下),用简化的术语来重新指定问题,瞧! 我们有解决scheme。

一个例子,一个函数来计算列表的长度(一个归纳定义的recursion数据types)。 假设它被定义,并且返回列表的长度,不出意外。 它必须遵守哪些法律? 在什么简化的问题下保存了什么不变?

最直接的是把它的头部元素分开,其余的 – 也就是列表尾部(根据如何定义/构build列表)。 法律是,

 length (x:xs) = 1 + length xs 

D'呃! 但是空的清单呢? 一定是那个

 length [] = 0 

那么我们如何编写这样一个函数?…等等…我们已经写好了! (在Haskell中,如果您想知道函数应用是通过并列表示的,括号仅用于分组,而(x:xs)是其中第一个元素为x的列表, x是其余的)。

我们所需要的一种语言来允许这样的编程风格,就是它拥有TCO (也许有点奢侈, TRMCO ),所以没有堆栈爆炸,我们已经设定好了。


另一件事是纯度 – 代码variables和/或数据结构的不可变性(logging的字段等)。

除了让我们的头脑不必跟踪什么时候在改变什么之外,这样做是什么呢,是否在我们的代码中明确地显示了时间,而不是隐藏在我们的“变化​​的”可变variables/数据中。 我们只能从命令代码中“改变”一个variables的价值 – 我们不能很好地改变它过去的价值,我们能吗?

所以我们最后列出了logging的变化历史logging,在代码中显然明显的变化:代替x := x + 2我们写成let x2 = x1 + 2 。 它使得关于代码的推理变得更容易。


为了在TCO尾部recursion的情况下解决不可变性问题,请考虑在累加器参数范例下对上述函数length进行尾recursion重写:

 length xs = length2 0 xs -- the invariant: length2 a [] = a -- 1st arg plus length2 a (x:xs) = length2 (a+1) xs -- length of 2nd arg 

这里的TCO除了直接跳转之外还意味着调用帧的重用,所以length [1,2,3]的调用链可以被看作是实际上改变了与函数参数相对应的调用栈帧的条目:

 length [1,2,3] length2 0 [1,2,3] -- a=0 (x:xs)=[1,2,3] length2 1 [2,3] -- a=1 (x:xs)=[2,3] length2 2 [3] -- a=2 (x:xs)=[3] length2 3 [] -- a=3 (x:xs)=[] 3 

在纯语言中,没有任何价值变异的原语,expression变化的唯一方式是将更新的值作为parameter passing给函数 ,以便进一步处理。 如果进一步的处理和以前一样,那么当然我们必须调用相同的函数,把更新后的值作为parameter passing给它。 这是recursion。


下面是整个计算参数列表长度的历史logging(如果需要,可以重用):

 length xs = last results where results = length3 0 xs length3 a [] = [a] length3 a (x:xs) = a : length3 (a+1) xs 

在Haskell中,这个variables被称为守护recursion或者corecursion(至less我认为是这样)。

recursion没有什么特别的。 它是编程和math方面的广泛工具,仅此而已。 但是,function语言通常是简约的。 他们的确引入了模式匹配,types系统,列表理解等许多花哨的概念,但它不过是一般而强大的语法糖,而是简单而原始的工具。 这个工具是:函数抽象和函数应用。 这是有意识的select,因为语言核心的简单性使得对它的推理更容易。 这也使编写编译器变得更容易。 用这个工具来描述循环的唯一方法是使用recursion,所以命令式程序员可能会认为函数式编程是关于recursion的。 不是的,它只是模仿那些穷人的花式循环,不能把这个语法糖放在goto语句上,所以这是他们第一个插入的东西。

需要(可能是间接的)recursion的另一点是处理recursion定义的数据结构。 最常见的例子是ADT list 。 在FP中它通常被定义为像这样的data List a = Nil | Branch a (List a) data List a = Nil | Branch a (List a) 。 由于这里的ADT的定义是recursion的,所以它的处理函数也应该是recursion的。 同样,recursion在这里也不是特别的:以recursion方式处理这样的ADT在命令式语言和函数式语言中都是自然而然的。 那么,如果列表式的ADT势在必行的话,仍然可以采用,但是如果是不同的树状结构,他们不能。

所以在recursion中没有什么特别的。 它只是另一种types的function应用程序。 但是,由于现代计算机系统的局限性(来自于C语言的devise决策不足,即事实上的标准跨平台汇编程序),函数调用即使是尾调用也不能无限嵌套。 正因为如此,function性编程语言的devise者必须限制允许的尾部调用尾部recursion(scala)或使用复杂的技术(如旧的ghc codegen)或直接编译到asm(现代的ghc codegen)。

TL; DR:在FP中recursion没有什么特别的地方,至less在IP中是这样,然而,由于JVM的限制,尾recursion是scala中允许的唯一的tail调用types。

避免副作用是函数式编程的一个支柱(另一个是使用更高阶的函数)。

想象一下如何在依赖变异的情况下利用命令式stream量控制。 可能吗?

当然for (var i = 0; i < 10; i++) ...取决于变异( i++ )。 实际上,任何条件循环结构都可以。 在while (something) ... something将取决于一些可变的状态。 当然, while (true) ...不使用突变。 但是如果你想摆脱这个循环,你将需要一个if (something) break 。 实际上,试图想到除了不依赖于变异的recursion之外的(非无限的)循环机制。

怎么样for (var x in someCollection) ... ? 现在我们正在接近函数式编程。 x可以被认为是循环体的一个参数 。 重新使用名称与重新分配值不同。 也许在循环的主体中,你可以用x (没有变异)的formsyield return值作为expression式。

完全等价地,您可以将for循环的主体移动到函数foo (x) ...的主体中,然后使用高阶函数将其映射到someCollection – 用Map(foo, someCollection)

但是,那么库函数Map如何实现而不会发生突变? 那么,当然使用recursion! 这是为你做的。 一旦开始使用第二个高阶函数replace循环结构,自己实现recursion函数就变得不常见了。

另外,在调用尾部优化的时候,recursion定义相当于一个迭代过程。 您也可以享受这篇博文: http : //blogs.msdn.com/b/ashleyf/archive/2010/02/06/recursion-is-the-new-iteration.aspx

有两个属性我认为是函数式编程必不可less的:

  1. 函数是第一类成员(只有相关的,因为使它有用的第二个属性是需要的)

  2. 函数是纯的,即用相同的参数调用的函数返回相同的值。

现在,如果你以强制性的风格编程,你必须使用赋值。

考虑一个for循环。 它有一个索引,每次迭代索引都有不同的值。 所以你可以定义一个返回这个索引的函数。 如果你两次调用这个函数,你可能会得到不同的结果。 从而打破了2号原则。

如果你违反原则,否则传递函数(原则1)会变得非常危险,因为现在函数的结果可能取决于调用函数的时间和频率。

上次我使用一种函数式语言(Clojure),我甚至从来没有使用recursion。 所有的东西都可以作为一组东西来处理,一个function被应用于获得部分产品,而另一个function被应用,直到达到最终结果。

recursion只是一个方法,而不一定是最清楚的方式,处理你通常必须处理的多个项目来处理任何g

为了新的FP学习者,我想补充我的2分钱。正如在一些答案中提到的,recursion是他们利用不可变的variables,但为什么我们需要这样做呢? 因为它可以很容易地在多个内核上并行运行程序,但是为什么我们要这么做呢? 我们不能在单核心上运行它,并像往常一样快乐吗? 否,因为处理的内容日益增加,并且CPU时钟周期不能比增加更多内核显着增加。 从过去的十年中,消费电脑的时钟速度已经高达2.7GHz到3.0GHz,而芯片devise人员在安装越来越多的晶体pipe方面存在问题。另外,FP已经是很长时间了,但是没有拿起因为它使用recursion和内存在当时是非常昂贵的,但随着时钟速度一年比一年高,所以社区决定继续与OOP编辑:这是相当快,我只有几分钟