功能编程
在计算机科学中,功能编程是一个编程范式,通过应用和组成功能来构建程序。这是一个声明的编程范式,其中函数定义是映射值的表达式树的树,而不是更新程序运行状态的命令式语句的顺序。
在功能编程中,功能被视为一流的公民,这意味着它们可以绑定到名称(包括本地标识符),作为参数传递并从其他功能返回,就像任何其他数据类型一样。这允许程序以声明性且可组合的样式编写,其中小功能以模块化方式组合在一起。
功能编程有时被视为纯粹功能编程的代名词,该功能编程的子集将所有函数视为确定性的数学函数或纯函数。当调用一些给定参数的纯函数时,它将始终返回相同的结果,并且不能受任何可变状态或其他副作用的影响。这与不纯的过程相反,不纯粹的程序(在命令式编程中常见)可能具有副作用(例如修改程序的状态或从用户那里获取输入)。纯粹功能编程的支持者声称,通过限制副作用,程序可以更少的错误,更易于调试和测试,并且更适合正式验证。
功能编程起源于学术界,从lambda conculus演变,这是一种基于功能的正式计算系统。历史上,功能性编程比当务之急的编程不那么受欢迎,但是许多功能性语言在当今的行业和教育中都在使用,包括常见的LISP ,方案, Clojure , Wolfram语言,球拍, Erlang ,Erlang, Elixir , Ocaml ,Haskell, Haskell和F# 。功能编程也是一些在特定领域中获得成功的某些语言的关键,例如Web中的JavaScript , r中的r中的r中的r in in statistics, j , k和q在财务分析中, xquery / xslt for xml 。 SQL和LEX / YACC (例如SQL和LEX / YACC)的域名语言使用功能编程的某些元素,例如不允许可变值。此外,许多其他编程语言都以功能风格支持编程或已实现功能编程的功能,例如C ++ 11 , C# , Kotlin , Perl , Php ,Php, Python , Go , Rust ,Raku, Raku , Scala和Java (自从Java 8) 。
历史
lambda微积分于1930年代由Alonzo Church开发,是一种由功能应用程序构建的正式计算系统。 1937年,艾伦·图灵(Alan Turing)证明了Lambda微积分和图灵机是等效的计算模型,表明Lambda演算已经完成。 lambda演算构成了所有功能编程语言的基础。 1920年代和1930年代, MosesSchönfinkel和Haskell Curry开发了相同的理论配方,即组合逻辑。
教堂后来开发了一个较弱的系统,即简单的lambda微积分,该系统通过将数据类型分配给所有术语来扩展Lambda微积分。这构成了静态键入功能编程的基础。
第一种高级功能编程语言LISP是在1950年代后期针对约翰·麦卡锡(John McCarthy)在马萨诸塞州理工学院(MIT)的IBM 700/7000系列科学计算机开发的。使用教堂的lambda符号定义了LISP功能,并使用标签结构扩展,以允许递归功能。 LISP首先引入了功能编程的许多范式特征,尽管早期的LISP是多范式语言,并且随着新范式的发展,对众多编程样式的支持。后来的方言,例如方案和clojure ,以及诸如Dylan和Julia之类的分支,试图简化和合理化LISP围绕一个干净的功能性核心,而Common Lisp旨在保留和更新其替换众多旧方言的范式特征。
信息处理语言(IPL),1956年,有时被认为是第一个基于计算机的功能编程语言。这是一种用于操纵符号列表的组装语言。它确实具有生成器的概念,该概念等于接受函数作为参数的函数,并且,由于它是汇编级的语言,因此代码可以是数据,因此IPL可以被视为具有高阶函数。但是,它在很大程度上依赖于突变列表结构和类似的命令特征。
肯尼斯·E·艾弗森( Kenneth E.ISBN 9780471430148)。 APL是对John Backus的FP的主要影响。在1990年代初,艾弗森(Iverson)和罗杰(Roger Hui)创建了J。在1990年代中期,以前与艾弗森(Iverson)合作的亚瑟·惠特尼(Arthur Whitney)创建了K ,该K及其后代Q。
在1960年代中期,彼得·兰丁(Peter Landin)发明了SECD机器,这是功能编程语言的第一台抽像机器,描述了Algol 60和Lambda Colculus之间的对应关系,并提出了ISWIM编程语言。
约翰·贝克斯(John Backus)在1977年的图灵奖演讲中介绍了FP “可以从冯·诺伊曼(Von Neumann)的风格中解放出编程吗?他将功能程序定义为以层次结构的方式来构建,以“组合”允许“程序代数”的“组合”;在现代语言中,这意味着功能程序遵循组成性的原则。 Backus的论文推广了对功能编程的研究,尽管它强调了功能级编程,而不是现在与功能编程相关的Lambda-Calculus样式。
1973年的语言ML是由爱丁堡大学的罗宾·米尔纳(Robin Milner)创建的,大卫·特纳(David Turner)在圣安德鲁斯大学开发了SASL语言。同样在1970年代的爱丁堡,Burstall和Darlington开发了功能性语言NPL 。 NPL基于Kleene递归方程,最初是在他们在程序转换方面的工作中引入的。然后,Burstall,Macqueen和Sannella合并了来自ML的多态性检查,以产生希望。 ML最终发展为几种方言,其中最常见的是OCAML和标准ML 。
在1970年代,盖伊·斯蒂尔( Guy L.方案是LISP使用词汇范围并需要尾声优化的第一个方言,这是鼓励功能编程的功能。
在1980年代, Per Martin-Löf开发了直觉类型理论(也称为建设性类型理论),该理论将功能程序与以依赖类型表示的建设性证明相关联。这导致了互动定理证明的新方法,并影响了随后的功能编程语言的发展。
戴维·特纳(David Turner)开发的懒惰功能语言米兰达( Miranda)最初出现在1985年,对哈斯克尔(Haskell)产生了强烈的影响。米兰达(Miranda)是专有的,哈斯克尔(Haskell)于1987年开始达成共识,构成了功能编程研究的开放标准。截至1990年,实施版本一直在进行。
最近,它发现在构建基于CGAL框架的OpenSCAD语言中的诸如参数CAD之类的壁ches中,尽管其对重新分配值的限制(所有值都视为常数)导致在不熟悉功能编程的用户中的混淆概念。
功能编程继续用于商业环境。
概念
许多概念和范式是针对功能编程的,通常是急需编程(包括面向对象的编程)。但是,编程语言通常适合几种编程范式,因此使用“大多数命令”语言的程序员可能已经利用了其中一些概念。
一流和高阶功能
高阶功能是可以将其他函数作为参数或结果返回结果的函数。在微积分中,高阶函数的一个示例是差分运算符,它返回函数的导数。
高阶功能与一流的功能密切相关,因为高阶功能和一流的功能都允许函数作为其他功能的参数和结果。两者之间的区别是微妙的:“高阶”描述了在其他功能上运行的功能的数学概念,而“一流”是对其使用无限制的编程语言实体的计算机科学术语(因此首先-Class函数可以出现在其他一流实体(例如数字)中的程序中,包括作为对其他函数的参数以及其返回值)。
高阶函数启用部分应用程序或咖哩,该技术一次将函数应用于其参数,每个应用程序都返回一个接受下一个参数的新函数。这样一来,程序员就可以简洁地表达连续功能作为添加运算符部分应用于自然数字第一的操作员。
纯函数
纯函数(或表达式)没有副作用(内存或I/O)。这意味着纯函数具有几个有用的属性,其中许多属性可用于优化代码:
- 如果未使用纯表达的结果,则可以在不影响其他表达式的情况下将其删除。
- 如果调用没有引起副作用的参数调用纯函数,则结果相对于该参数列表(有时称为引用透明度或依从性)是恒定的,即,与相同参数相同的结果再次称为纯函数。 (这可以实现缓存优化,例如记忆。)
- 如果两个纯表达式之间没有数据依赖性,则可以逆转它们的顺序,也可以并行执行,并且它们不能相互干扰(其他术语,对任何纯表达式的评估都是线程安全的)。
- 如果整个语言不允许副作用,则可以使用任何评估策略。这使编译器可以自由重新排序或结合程序中表达式评估(例如,使用砍伐森林)。
虽然大多数用于编程语言的编译器都检测到纯函数并为纯函数呼叫执行通用表达式消除,但他们不能总是为预编译的库,这些库通常不会公开此信息,从而防止涉及这些外部功能的优化。一些编译器(例如GCC )为程序员添加了额外的关键字,以将外部功能明确标记为纯粹的功能,以实现此类优化。 Fortran 95还可以指定功能被指定为纯净。 C ++ 11添加constexpr
关键字具有类似的语义。
递回
功能语言中的迭代(循环)通常是通过递归完成的。递归功能调用自己,让操作重复直到达到基本情况。通常,递归需要保持堆栈,该堆栈以线性量消耗到递归深度的线性。这可能会使递归的使用价格过高,而不是命令循环。但是,可以通过编译器识别和优化一种特殊形式的递归形式,将其识别为与用命令性语言实施迭代相同的代码。可以通过将程序转换为编译过程中的延续传递样式来实现尾部递归优化,以及其他方法。
方案语言标准需要实施以支持适当的尾部递归,这意味着它们必须允许无限数量的主动尾部调用。适当的尾随不仅是一种优化。这是一种语言功能,可以向用户保证可以使用递归表达循环,而这样做将是安全空间。此外,与其名称相反,它说明了所有尾声,而不仅仅是尾部递归。尽管通常通过将代码转换为命令循环来实现适当的尾部递归,但实现可能会以其他方式实施。例如,鸡肉有意保持堆栈,并让堆栈溢出。但是,当发生这种情况时,它的垃圾收集器将索取空间,即使它不会将尾部递归变成循环,也可以允许无限的主动尾部呼叫。
可以使用高阶函数将递归的常见模式抽象,具有语态和变形(或“折叠”和“ Frolls”)是最明显的例子。这种递归方案起着类似于内置控制结构的作用,例如命令式语言中的循环。
大多数通用功能编程语言允许不受限制的递归并完成图灵的完成,这使得无法确定的停止问题可能会导致方程推理的不健全性,并且通常需要将不一致引入语言类型系统表达的逻辑中。某些特殊目的语言(例如COQ)仅允许结构良好的递归并非常正常化(不终止计算只能用无限的值(称为CODATA )来表示)。结果,这些语言无法完成,并且在其中表达某些功能是不可能的,但是它们仍然可以表达一系列有趣的计算,同时避免了不受限制的递归引入的问题。功能编程仅限于结构递归的递归以及其他一些约束,称为总功能编程。
严格与非分数评估
功能语言可以通过使用严格的(急切)或非描述(懒惰)评估来对其进行分类,这些概念是指在评估表达式时如何处理函数参数。技术差异在于包含失败或不同计算的表达式语义中。在严格的评估下,对包含未衰弱失败的任何术语的评估失败。例如,表达式:
print length([2+1, 3*2, 1/0, 5-4])
由于列表的第三个要素中的零,因此在严格的评估下失败。在懒惰的评估下,长度函数返回值4(即,列表中的项目数),因为评估它不会试图评估构成列表的术语。简而言之,严格的评估始终在调用函数之前完全评估函数参数。懒惰评估不会评估函数参数,除非需要其值来评估函数调用本身。
用功能语言评估懒惰评估的通常实施策略是减少图形。默认情况下,用几种纯粹的功能语言使用懒惰评估,包括Miranda , Clean和Haskell 。
休斯(Hughes)1984年主张懒惰评估,作为通过放大数据流的生产者和消费者的独立实施来改善计划模块化的一种机制。 1993年的Launchbury介绍了懒惰评估引入的一些困难,尤其是在分析程序的存储要求时,并提出了一种操作语义来帮助进行此类分析。 Harper 2009提出,使用该语言的类型系统将其区分开来,包括以相同语言的严格和懒惰评估。
类型系统
尤其是自1970年代的Hindley -Milner类型推理的发展,功能性编程语言倾向于使用键入的lambda cyculus ,在编译时间拒绝所有无效的程序,并冒着误报的危险,而不是接受所有有效的lambda colculus在汇编时间的程序和LISP及其变体(例如方案)中使用的虚假负面错误风险,因为当信息足以不拒绝有效程序时,它们会在运行时拒绝所有无效的程序。代数数据类型的使用使操纵复杂的数据结构方便;强大的编译时类型检查的存在使程序在没有其他可靠性技术(例如测试驱动的开发)的情况下更可靠,而Type推理则使程序员从大多数情况下需要手动向编译器手动将类型声明类型释放。
一些面向研究的功能语言,例如COQ , AGDA , Cayenne和Epigram ,基于直觉类型理论,该理论可以依靠术语。这样的类型称为因类型。这些类型的系统没有可确定的推理,并且很难理解和编程。但是因类型可以在高阶逻辑中表达任意命题。然后,通过咖哩 - 纽带同构,这些语言中良好的程序成为编写正式数学证据的一种手段,编译器可以从中生成经过认证的代码。尽管这些语言主要在学术研究中感兴趣(包括在形式上的数学中),但它们也开始用于工程学。 CompCert是用COQ编写并正式验证的C编程语言子集的编译器。
可以以一种有限的形式称为通用代数数据类型(GADT),可以通过提供相关键入编程的一些好处,同时避免其大部分不便。 GADT在Glasgow Haskell编译器, OCAML和Scala中可用,并已被提议作为其他语言的补充,包括Java和C#。
参照透明度
功能程序没有分配语句,即功能程序中变量的值一旦定义就永远不会更改。这消除了任何副作用的机会,因为在任何执行点都可以用其实际值替换任何变量。因此,功能程序是参考透明的。
考虑C分配声明x=x * 10
,这更改了分配给变量的值x
。让我们说的是,初始价值x
曾是1
,然后对变量进行两个连续评估x
产量10
和100
分别。显然,取代x=x * 10
两个10
或者100
给程序具有不同的含义,因此该表达式不是参考透明的。实际上,任务语句从来都不是透明的。
现在,考虑另一个功能,例如int plusone(int x) {return x+1;}
是透明的,因为它不会隐式更改输入X,因此没有这种副作用。功能程序专门使用此类型的函数,因此是参考透明的。
数据结构
纯粹的功能数据结构通常以与其命令的方式不同。例如,具有恒定访问和更新时间的数组是大多数命令语言的基本组成部分,许多命令式数据结构(例如哈希表和二进制堆)基于数组。数组可以用地图或随机访问列表替换,这些列表纯粹是功能实现,但具有对数访问和更新时间。纯粹的功能数据结构具有持久性,这是使数据结构的先前版本未经修改的属性。在Clojure中,持续的数据结构用作其命令式同行的功能替代方法。例如,持续的向量使用树进行部分更新。调用插入方法将导致创建一些但不是所有节点。
与命令编程进行比较
功能编程与命令式编程大不相同。最重大的差异源于以下事实:功能编程避免了副作用,副作用在命令编程中用于实施状态和I/O。纯功能编程完全阻止了副作用并提供了参考透明度。
高阶功能很少用于较旧的命令编程。传统的命令程序可能会使用循环来遍历和修改列表。另一方面,功能程序可能会使用更高阶的“映射”功能,该功能获取功能和列表,通过将函数应用于每个列表项目来生成和返回新列表。
命令与功能编程
以下两个示例(用JavaScript编写)达到了相同的效果:它们将所有偶数数字乘以10,并将它们添加全部,将最终总和存储在变量“结果”中。
传统的命令循环:
const numList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result = 0;
for (let i = 0; i < numList.length; i++) {
if (numList[i] % 2 === 0) {
result += numList[i] * 10;
}
}
具有高阶功能的功能编程:
const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter(n => n % 2 === 0)
.map(a => a * 10)
.reduce((a, b) => a + b, 0);
模拟状态
有一些任务(例如,维护银行帐户余额)通常最自然地通过状态实施。 Pure功能编程执行这些任务,以及I/O任务,例如以不同的方式接受用户输入和在屏幕上打印。
纯粹的功能编程语言haskell使用源自类别理论的单子实现它们。 MONAD提供了一种抽象某些类型的计算模式的方法,包括(但不限于)以可变状态(以及其他副作用(例如I/O)的计算建模,而不会失去纯度。虽然在适当的模板和示例给定适当的模板和示例的情况下,现有的单子可能很容易应用,但是当要求它们定义新的单调时(某些类型的图书馆需要有时需要),许多学生发现它们很难从概念上理解,例如。
功能语言还通过不变状态模拟状态。这可以通过使函数接受状态作为其参数之一来完成,并返回新状态以及结果,使旧状态保持不变。
不纯净的功能语言通常包括一种更直接的可变状态的方法。例如, Clojure使用可以通过将纯函数应用于当前状态来更新的托管参考。这种方法可实现可变性,同时仍将纯函数用作表达计算的首选方法。
已经开发出诸如Hoare逻辑和独特性之类的替代方法来跟踪程序中的副作用。一些现代研究语言使用效果系统来明确地存在副作用。
效率问题
功能编程语言通常比C和Pascal等命令语言在使用CPU和内存方面的效率较低。这与以下事实有关:一些可变的数据结构(例如数组)使用当前硬件具有非常简单的实现。可以使用深层管道的CPU访问平坦阵列,并通过缓存(没有复杂的指针追逐)有效地预取,或使用SIMD说明进行处理。创建其同样有效的通用不可变的同类产品也不容易。对于纯粹的功能性语言,最坏的案例放缓在所使用的内存单元的数量中是对数,因为可变内存可以由具有对数访问时间(例如平衡树)的纯粹功能数据结构来表示。但是,这种放缓并不是普遍的。对于执行密集数值计算的程序,根据计算机语言基准游戏,诸如OCAML和CLEAN之类的功能性语言仅比C稍慢一些。对于处理大型矩阵和多维数据库的程序,设计了速度优化的数组功能语言(例如J和K )。
在许多情况下,数据的不变性可以通过允许编译器做出不安全语言的假设来提高执行效率,从而增加内联扩展的机会。
懒惰的评估也可能会加快程序的速度,即使是渐进的,而它最多可能会以恒定的因素降低(但是,如果使用不正确,它可能会引入内存泄漏)。 1993年的Launchbury讨论了与懒惰评估的记忆泄漏有关的理论问题,以及O'Sullivan等人。 2008提供一些实用建议,以分析和修复它们。但是,懒惰评估的最普遍的实施是广泛利用了用深层管道和多层缓存的现代处理器(缓存失踪可能会花费数百个周期)的现代处理器的效果较差。
非功能语言的功能编程
可以在传统上不被视为功能性语言的语言中使用功能性编程风格。例如, D和Fortran 95明确支持纯函数。
JavaScript , Lua , Python和Go从他们的成立起就具有头等舱功能。 Python支持“ Lambda ”,“ Map ”,“减少”和“滤波器”,以及Python 2.2中的封闭,尽管Python 3降级为“降低”到functools
标准库模块。一流的功能已引入其他主流语言,例如PHP 5.3, Visual Basic 9 , C# 3.0, C ++ 11和Kotlin 。
在PHP中,匿名课程,关闭和Lambdas得到了充分的支持。正在开发用于不变数据结构的库和语言扩展,以帮助以功能风格的方式进行编程。
在Java中,有时可以使用匿名类来模拟封闭。但是,匿名课程并不总是适当的替换,因为它们的功能更有限。 Java 8支持Lambda的表达方式,以替代某些匿名课程。
在C#中,不需要匿名类,因为封闭和lambdas得到了充分的支持。为不可变数据结构开发了库和语言扩展,以辅助C#功能风格的编程。
许多面向对象的设计模式在功能编程术语中表达出来:例如,策略模式只是决定使用高阶功能,而访客模式大致对应于血清态或折叠。
同样,功能编程的不可变数据的想法通常包含在命令性编程语言中,例如python中的元组,这是一个不变的数组,而JavaScript中的object.freeze()。
与逻辑编程进行比较
逻辑编程可以看作是功能编程的概括,在该编程中,功能是一种特殊情况。例如,功能,母亲(x)= y,(每个x只有一个母亲y)可以由关系母亲(x,y)表示。函数具有严格的参数输入输出模式,但可以使用任何输入和输出模式查询关系。考虑以下逻辑程序:
mother(charles, elizabeth).
mother(harry, diana).
可以像功能性计划一样查询该计划,以从孩子那里产生母亲:
?- mother(harry, X).
X = diana
?- mother(charles, X).
X = elizabeth
但也可以向后询问,生成孩子:
?- mother(X, elizabeth).
X = charles
?- mother(X, diana).
X = harry
它甚至可以用来生成母亲关系的所有实例:
?- mother(X, Y).
X = charles,
Y = elizabeth
X = harry,
Y = diana
与关系语法相比,函数语法是嵌套函数的更紧凑的符号。例如,功能性语法中外婆的定义可以以嵌套形式写入:
maternal_grandmother(X) = mother(mother(X)).
关系符号中相同的定义需要以未命名的形式编写:
maternal_grandmother(X, Y) :- mother(X, Z), mother(Z, Y).
这里:-
意味着如果和 ,
手段和。
但是,两种表示之间的差异只是句法。在CIAO Prolog中,可以嵌套关系,例如功能编程中的功能:
grandparent(X) := parent(parent(X)).
parent(X) := mother(X).
parent(X) := father(X).
mother(charles) := elizabeth.
father(charles) := phillip.
mother(harry) := diana.
father(harry) := charles.
?- grandparent(X,Y).
X = harry,
Y = elizabeth? ;
X = harry,
Y = phillip ? ;
CIAO将类似功能的符号转换为关系形式,并使用标准Prolog Excution策略执行所得的逻辑程序。
申请
试算表
电子表格可以视为一种纯,零级,严格评估功能编程系统的形式。但是,电子表格通常缺乏高阶功能以及代码重用,在某些实现中,也缺乏递归。已经为电子表格程序开发了几个扩展程序,以实现高阶和可重复使用的功能,但到目前为止,本质上仍主要是学术性的。
学术界
功能编程是编程语言理论领域的研究领域。有几个由同行评审的出版物场所着眼于功能编程,包括国际功能编程会议,功能编程杂志和功能编程趋势研讨会。
行业
功能编程已在广泛的工业应用中使用。例如,由瑞典公司Ericsson在1980年代后期开发的Erlang最初被用来实施耐心的电信系统,但此后一直很受欢迎,因为他在Nortel , Facebook , Electricitédedeelecterricitédee的公司中建立了一系列应用程序而变得很受欢迎。法国和WhatsApp 。方案是LISP的方言,用作早期Apple Macintosh计算机上多个应用程序的基础,并已应用于诸如训练模拟软件和望远镜控制等问题。 OCAML于1990年代中期推出,在财务分析,驾驶员验证,工业机器人编程和嵌入式软件的静态分析等领域中看到了商业用途。 Haskell虽然最初是作为一种研究语言,但也已应用于航空系统,硬件设计和Web编程等领域。
在行业中使用的其他功能编程语言包括Scala , F# , Wolfram语言, LISP ,标准ML和Clojure。
功能“平台”在风险分析(尤其是大型投资银行)的金融中很受欢迎。风险因素被编码为形成相互依存图(类别)的功能,以测量市场转移的相关性,与Gröbner基础优化的方式相似,也用于法规框架,例如综合资本分析和审查。鉴于在金融中使用OCAML和CAML变化,这些系统有时被认为与分类的抽像机器有关。功能编程受类别理论的严重影响。
教育
许多大学教功能编程。有些人将其视为介绍性编程概念,而另一些人则将其视为命令式编程方法。
在计算机科学之外,功能编程用于教授解决问题,代数和几何概念。它也被用来教古典力学,如书籍结构和古典力学的解释。