走进函数式编程

最近在看《CoderAtWorks》时发现编程界的大佬们几乎都对函数式编程很是推崇,因而就很是好奇函数式编程究竟是什么东西,搜索引擎查了一堆资料,算是半懂了吧,因而就借此总结下学习的东西。程序员

手机上写Markdown绝对是一种折磨,排版仍是等回家再说吧。编程

1.渊源

20世纪30年代普林斯顿大学有四位学者, 艾伦·图灵 、 约翰·冯·诺伊曼 、 库尔特·哥德尔 、 阿隆佐·邱奇 ,他们都对形式系统感兴趣,相对于现实世界,他们更关心如何解决抽象的数学问题。而他们的问题都有这么一个共同点:都在尝试解答关于计算的问题。诸如:若是有一台拥有无穷计算能力的超级机器,能够用来解决什么问题?它能够自动的解决这些问题吗?是否是仍是有些问题解决不了,若是有的话,是为何?若是这样的机器采用不一样的设计,它们的计算能力相同吗?数组

在与这些人的合做下,阿隆佐设计了一个名为 lambda演算 的形式系统。这个系统实质上是为其中一个超级机器设计的编程语言。在这种语言里面,函数的参数是函数,返回值也是函数。这种函数用希腊字母lambda(λ),这种系统所以得名。有了这种形式系统,阿隆佐终于能够分析前面的那些问题而且可以给出答案了。闭包

除了阿隆佐·邱奇,艾伦·图灵也在进行相似的研究。他设计了一种彻底不一样的系统(后来被称为图灵机),并用这种系统得出了和阿隆佐类似的答案。到了后来人们证实了图灵机和 lambda演算 的能力是同样的。架构

1949年第一台电子离散变量自动计算机诞生并取得了巨大的成功。它是冯·诺伊曼设计架构的第一个实例,也是一台现实世界中实现的图灵机。相比他的这些同事,那个时候阿隆佐的运气就没那么好了。并发

到了50年代末,一个叫John McCarthy的MIT教授(他也是普林斯顿的硕士)对阿隆佐的成果产生了兴趣。1958年他发明了一种列表处理语言(Lisp),这种语言是一种阿隆佐lambda演算在现实世界的实现,并且它能在冯·诺伊曼计算机上运行!不少计算机科学家都认识到了Lisp强大的能力。1973年在MIT人工智能实验室的一些程序员研发出一种机器,并把它叫作Lisp机。因而阿隆佐的 lambda演算 也有本身的硬件实现了!编程语言

2.定义

Lisp 诞生以后,新的函数式编程语言层出不穷,例如 Erlang 、 clojure 、 Scala 、 F# 等等。目前最当红的 Python 、 Ruby 、 Javascript ,对函数式编程的支持都很强,就连老牌的面向对象的 Java 、面向过程的 PHP ,都忙不迭地加入对匿名函数的支持。函数式编程

简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。函数

它属于"结构化编程"的一种,主要思想是把运算过程尽可能写成一系列嵌套的函数调用。举例来讲,如今有这样一个数学表达式:工具

'''

(1 + 2) * 3 - 4

'''

传统的过程式编程,可能这样写:

'''

var a = 1 + 2;

var b = a * 3;

var c = b - 4;

'''

函数式编程要求使用函数,咱们能够把运算过程定义为不一样的函数,而后写成下面这样:

'''

var res = subtract(multiply(add(1,2), 3), 4);

'''

这就是函数式编程。

3.特色

函数是一等公民

所谓"第一等公民"(first class),指的是函数与其余数据类型同样,处于平等地位,能够赋值给其余变量,也能够做为参数,传入另外一个函数,或者做为别的函数的返回值。

举例来讲,下面代码中的print变量就是一个函数,能够做为另外一个函数的参数。

'''

var print = function(i){ console.log(i);};

[1,2,3].forEach(print);

'''

不可改变量

在函数式编程中,咱们一般理解的变量在函数式编程中也被函数代替了:在函数式编程中变量仅仅表明某个表达式。这里所说的‘变量’是不能被修改的。全部的变量只能被赋一次初值。在Java中就意味着每个变量都将被声明为final(若是你用C++,就是const)。在函数式编程中,没有非final的变量。

final int i = 5;

final int j = i + 3;

无状态

若是变量不能够改变,那么状态如何存储,这个不用担忧,函数式编程中状态经过函数来保存,若是你须要保存一个状态一段时间而且时不时的修改它,那么你能够编写一个递归函数。举个例子,试着写一个函数,用来反转一个字符串。

function reverse(String arg) {

if(arg.length == 0) {

return arg;

} else {

return reverse(arg.substring(1, arg.length)) + arg.substring(0, 1);

}

}

因为使用了递归,函数式语言的运行速度比较慢,这是它长期不能在业界推广的主要缘由。

技术

map & reduce

map 和 reduce 开篇时已经提到过,他们是对一个集合最经常使用的操做。

map 接受一个集合和一个函数f,集合中每一个元素都映射到函数f上,并返回一个新的集合,简单实现以下(详见 mdn map polyfill ):

function map(arr, callback){

var l = arr && arr.length || 0,

out = [];

for (var i = 0; i < l; i++) {

out[i] = callback(arr[i]);

}

return out;

}

reduce 接受一个集合和一个函数f,而后将f映射到数组的相邻的两个元素上,简单实现以下(详见 mdn reduce polyfill ):

function reduce(arr, callback, b){

var l = arr && arr.length || 0,

x = 0;

b = b || 0;

for (var i = 0; i < l; i++) {

x = callback(arr[i], x);

}

return x;

}

柯里化

柯里化就是把一个函数的多个参数分解成多个函数, 而后把函数多层封装起来,每层函数都返回一个函数去接收下一个参数这样,能够简化函数的多个参数。

例如要计算一个数的平方,能够先实现一个计算任意整数次幂的函数,而后调用接口实现计算一个数的平方:

function pow(base, p) {/计算base的p次方/}

function square(a) {

return pow(a, 2);

}

柯里化就是这么简单:一种能够快速且简单的实现函数封装的捷径。咱们能够更专一于本身的设计,编译器则会为你编写正确的代码!何时使用currying呢?很简单,当你想要用适配器模式(或是封装函数)的时候,就是用currying的时候。对于函数编程来讲,适配器模式就是多余的。

惰性求值

在指令式语言中如下代码会按顺序执行,因为每一个函数都有可能改动或者依赖于其外部的状态,所以必须顺序执行。先是计算 somewhatLongOperation1 ,而后到 somewhatLongOperation2 ,最后执行 concatenate 。假如把 concatenate 换成另一个函数,这个函数中有条件判断语句并且实际上只会须要两个参数中的其中一个,那么就彻底没有必要执行计算另一个参数的函数了!

var s1 = somewhatLongOperation1();

var s2 = somewhatLongOperation2();

var s3 = concatenate(s1, s2);

函数式语言就不同了。只有到了执行须要 s1 、 s2 做为参数的函数的时候,才真正须要执行这两个函数。因而在 concatenate 这个函数没有执行以前,都没有须要去执行这两个函数:这些函数的执行能够一直推迟到 concatenate() 中须要用到s1和s2的时候。

惰性求值是十分强大的技术,可是须要编译器的支持。

高阶函数

高阶函数就是函数当参数,把传入的函数作一个封装,而后返回这个封装函数。例如咱们要实现一个计算1和任意数字的和的函数:

var partAdd = function(p1){

this.add = function (p2){

return p1 + p2;

};

return add;

};

var add = partAdd(1);

add(2); // 3

执行 partAdd(1) 时返回的任然是一个函数,当再次传入第二个参数时,就能够计算出和了。

上面的例子只是为了理解高阶函数,实际运用以下例所示:

var add = function(a,b){

return a + b;

};

function math(func,array){

return func(array[0],array[1]);

}

math(add,[1,2]); // 3

尾调用优化

尾调用的概念很是简单,一句话就能说清楚,就是指某个函数的最后一步是调用另外一个函数。

function f(x){

return g(x);

}

上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。

如下两种状况,都不属于尾调用。

// 状况一

function f(x){

let y = g(x);

return y;

}

// 状况二

function f(x){

return g(x) + 1;

}

上面代码中,状况一是调用函数g以后,还有别的操做,因此不属于尾调用,即便语义彻底同样。状况二也属于调用后还有操做,即便写在一行内。

咱们知道函数调用会在内存造成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。多层次的调用记录造成了调用栈。尾调用因为是函数的最后一步操做,因此不须要保留外层函数的调用记录,由于调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就能够了。

function f() {

let m = 1;

let n = 2;

return g(m + n);

}

f();

// 等同于

function f() {

return g(3);

}

f();

// 等同于

g(3);

函数调用自身,称为递归。若是尾调用自身,就称为尾递归。基于尾调用优化的原理,咱们能够对尾递归进行优化。递归须要保存大量的调用记录,很容易发生栈溢出错误,若是使用尾递归优化,将递归变为循环,那么只须要保存一个调用记录,这样就不会发生栈溢出错误了。

例如计算阶乘的函数:

// 不是尾递归,没法优化

function factorial(n) {

if (n === 1) return 1;

return n * factorial(n - 1);

}

// 尾递归,能够优化

function factorial(n, total) {

if (n === 1) return total;

return factorial(n - 1, n * total);

}

目前的ES5中并无规定尾调用优化,可是ES6中明确规定了必须实现尾调用优化,也就是ES6中只要使用尾递归,就不会发生栈溢出。因此 对于递归函数尽可能改写为尾递归形式 。

闭包

目前为止关于函数式编程各类功能的讨论都只局限在“纯”函数式语言范围内。不少这样的语言都不要求全部的变量必须为final,能够修改他们的值。也不要求函数只能依赖于它们的参数,而是能够读写函数外部的状态。同时这些语言又包含了函数编程的特性,如高阶函数。与在lambda演算限制下将函数做为参数传递不一样,在指令式语言中要作到一样的事情须要支持一个有趣的特性,人们常把它称为lexical closure。

看以下例子,虽然外层的 makePowerFn 函数执行完毕,栈上的调用帧被释放,可是堆上的做用域并不被释放,所以 power 依旧能够被 powerFn 函数访问,这样就造成了闭包:

function makePowerFn(power) {

function powerFn(base) {

return pow(base, power);

}

return powerFn;

}

var square = makePowerFn(2);

square(3); // 9

优势

代码简洁,易于理解

函数式编程大量使用函数,减小了代码的重复,所以程序比较短,开发速度较快。Paul Graham在《黑客与画家》一书中写道:一样功能的程序,极端状况下,Lisp代码的长度多是C代码的二十分之一。

函数式编程的自由度很高,能够写出很接近天然语言的代码。例如前文提到的 (1 + 2) * 3 - 4 的例子,写成函数时:

add(1,2).multiply(3).subtract(4);

容易调试

由于函数式编程中的每一个符号都是 final 的,因而没有什么函数会有反作用。谁也不能在运行时修改任何东西,也没有函数能够修改在它的做用域以外修改什么值给其余函数继续使用(在指令式编程中能够用类成员或是全局变量作到)。这意味着决定函数执行结果的惟一因素就是它的返回值,而影响其返回值的惟一因素就是它的参数。

若是一段FP程序没有按照预期设计那样运行,调试的工做几乎不费吹灰之力。这些错误是百分之一百能够重现的,由于FP程序中的错误不依赖于以前运行过的不相关的代码。

并发

函数式编程不须要考虑"死锁"(deadlock),由于它不修改变量,因此根本不存在"锁"线程的问题。没必要担忧一个线程的数据,被另外一个线程修改,因此能够很放心地把工做分摊到多个线程,部署"并发编程"(concurrency)。

仍是以前的例子:

var s1 = somewhatLongOperation1();

var s2 = somewhatLongOperation2();

var s3 = concatenate(s1, s2);

因为 s1 和 s2 互不干扰,不会修改变量,谁先执行是无所谓的,因此能够放心地增长线程,把它们分配在两个线程上完成。其余类型的语言就作不到这一点,由于s1可能会修改系统状态,而s2可能会用到这些状态,因此必须保证 s2 在 s1 以后运行,天然也就不能部署到其余线程上了。

热部署

函数式编程中全部状态就是传给函数的参数,而参数都是储存在栈上的。这一特性让软件的热部署变得十分简单。只要比较一下正在运行的代码以及新的代码得到一个diff,而后用这个diff更新现有的代码,新代码的热部署就完成了。其它的事情有FP的语言工具自动完成! Erlang 语言早就证实了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级固然是不能停机的。

相关文章
相关标签/搜索