函数式编程简介

函数式编程简介


一 、什么是函数式编程

函数式编程是一种编程范式,我们常见的编程范式有命令式编程(Imperative programming)函数式编程逻辑式编程,常见的面向对象编程是也是一种命令式编程。

命令式编程是面向计算机硬件的抽象,有变量(对应着存储单元),赋值语句(获取,存储指令),表达式(内存引用和算术运算)和控制语句(跳转指令),一句话,命令式程序就是一个冯诺依曼机的**指令序列**。

而函数式编程是面向数学的抽象,将计算描述为一种表达式求值,一句话,函数式程序就是一个表达式

js常规编程容易带来一些麻烦:使用可变状态(mutable state)、无限制副作用(unrestricted side effects)和无原则设计(unprincipled design)

函数式编程之前已经有一些编程规则了:

DRY,loose coupling high cohesion, Principle of least surprise,single responsibility

摘抄一个网上的一个海鸥程序的js编程例子:

// 范畴学
// 鸟群合并则变成了一个更大的鸟群,
// 繁殖则增加了鸟群的数量,增加的数量就是它们繁殖出来的海鸥的数量


var Flock = function(n) {
this.seagulls = n;
};
Flock.prototype.conjoin = function(other) {
this.seagulls += other.seagulls;
return this;
};
Flock.prototype.breed = function(other) {
this.seagulls = this.seagulls * other.seagulls;
return this;
};
var flock_a = new Flock(4);
var flock_b = new Flock(2);
var flock_c = new Flock(0);

var result = flock_a.conjoin(flock_c).breed(flock_b).conjoin(flock_a.breed(flock_b)).seagulls;

大家可以先看下这个程序有没有问题?

上面的方法逻辑已经非常少了,但是看的费力,状态和可变值也非常难以追踪。我们改写成更靠近函数式的方法

var conjoin = function(flock_x, flock_y) { return flock_x + floc
k_y };
var breed = function(flock_x, flock_y) { return flock_x * flock_
y };
var flock_a = 4;
var flock_b = 2;
var flock_c = 0;
var result = conjoin(breed(flock_b, conjoin(flock_a, flock_c)),
breed(flock_a, flock_b));

这个方法能让我们拿到正确的答案,但是函数的嵌套有点多,让人费解,代码组合可以解决这个问题

上面的方法从数学角度出发,就会发现执行的逻辑是在进行简单的加( conjoin ) 和乘( breed )运算而已

我们从数学的角度来改写下上面的逻辑:

var add = function(x, y) { return x + y };
var multiply = function(x, y) { return x * y };
var flock_a = 4;
var flock_b = 2;
var flock_c = 0;
var result = add(multiply(flock_b, add(flock_a, flock_c)), multiply(flock_a, flock_b));
//=>16

是不是有一种熟悉的感觉:

// 结合律(assosiative)
add(add(x, y), z) == add(x, add(y, z));

// 交换律(commutative)
add(x, y) == add(y, x);

// 同一律(identity)
add(x, 0) == x;

// 分配律(distributive)
multiply(x, add(y,z)) == add(multiply(x, y), multiply(x, z));

用这个规律来改写下海鸥程序:

// 原有代码
add(multiply(flock_b, add(flock_a, flock_c)), multiply(flock_a,flock_b));

// 应用同一律,去掉多余的加法操作(add(flock_a, flock_c) == flock_a)
add(multiply(flock_b, flock_a), multiply(flock_a, flock_b));

// 再应用分配律
multiply(flock_b, add(flock_a, flock_a));

二、 一等公民函数

在很多传统语言(C/C++/Java/C#等)中,函数都是作为一个二等公民存在,你只能用语言的关键字声明一个函数然后调用它,如果需要把函数作为参数传给另一个函数,或是赋值给一个本地变量,又或是作为返回值,就需要通过函数指针(function pointer)、代理(delegate)等特殊的方式周折一番。

而在JavaScript世界中函数却是一等公民,它不仅拥有一切传统函数的使用方式(声明和调用),而且可以做到像简单值一样赋值(var func = function(){})、传参(function func(x,callback){callback();})、返回(function(){return function(){}}),这样的函数也称之为第一级函数(First-class Function)。不仅如此,JavaScript中的函数还充当了类的构造函数的作用,同时又是一个Function类的实例(instance)。这样的多重身份让JavaScript的函数变得非常重要。

虽然在JavaScript 中,一等公民函数是基本概念,但仍然能找到很多对这个概念无视的案例

var hi = function(name){
	return "Hi " + name;
};
var greeting = function(name) {
	return hi(name);
};

// 来自github上的源码
var getServerStuff = function(callback){
	return ajaxCall(function(json){
		return callback(json);
	});
};

用一个函数把另一个函数包起来,目的仅仅是延迟执行,真的是非常糟糕的编程习惯,因为这种代码非常难以理解和维护

我们来改写下这个方法,把这个方法的真实意图表现出来:

// 这一个代码块
return ajaxCall(function(json){
	return callback(json);
});
// 等价于这行
return ajaxCall(callback);

// 那么,重构下 getServerStuff
var getServerStuff = function(callback){
	return ajaxCall(callback);
};
// ...就等于
var getServerStuff = ajaxCall; // <-- 看,没有括号哦

各位,以上才是写函数的正确方式

在看一个控制器例子:

var BlogController = (function() {
    
	var index = function(posts) {
		return Views.index(posts);
	};
    
	var show = function(post) {
		return Views.show(post);
	};
    
	var create = function(attrs) {
		return Db.create(attrs);
	};
    
	var update = function(post, attrs) {
		return Db.update(post, attrs);
	};
    
	var destroy = function(post) {
		return Db.destroy(post);
	};
    
	return {
        index: index, 
        show: show, 
        create: create, 
        update: update, 
        destroy: destroy
    };
})();

可以看到这里面大部分代码都是多余的,它完全可以写得更简洁明了

var BlogController = {
    index: Views.index, 
    show: Views.show, 
    create: Db.create, 
    update: Db.update, 
    destroy: Db.destroy
};

函数式编程之所以推崇一等公民函数,很多时候包装函数除了增加一些没有实际用处的间接层实现起来很容易,但这样做除了徒增代码量,提高维护和检索代码的成本外,没有任何用处。

此外,另外,如果一个函数被不必要地包裹起来了,而且发生了改动,那么包裹它的那个函数也要做相应的变更。例如:

httpGet('/post/2', function(json){
	return renderPost(json);
});

如果 httpGet 要改成可以抛出一个可能出现的 err 异常,那我们还要回过头去把里面函数也改了。

// 把整个应用里的所有 httpGet 调用都改成这样,可以传递 err 参数。
httpGet('/post/2', function(json, err){
	return renderPost(json, err);
});

写成一等公民函数的形式,要做的改动将会少得多:

httpGet('/post/2', renderPost); 
// renderPost 将会在 httpGet 中调用,想要多少参数都行

除了删除不必要的函数,正确地为参数命名也必不可少。当然命名不是什么大问题,但还是有可能存在一些不当的命名,尤其随着代码量的增长以及需求的变更,这种可能性也会增加。项目中常见的一种造成混淆的原因是,针对同一个概念使用不同的命名。还有通用代码的问题。比如,下面这两个函数做的事情一模一样后一个就显得更加通用,可重用性也更高:


// 只针对当前的博客
var validArticles = function(articles) {
	return articles.filter(function(article){
		return article !== null && article !== undefined;
	});
};
// 对未来的项目友好太多
var compact = function(xs) {
	return xs.filter(function(x) {
		return x !== null && x !== undefined;
	});
};

在命名的时候,我们特别容易把自己限定在特定的数据上(本例中是articles )。这种现象很常见,也是重复造轮子的一大原因。

还有一点也很重要,一定要非常小心 this 值,别让它反咬你一口,这一点与面向对象代码类似。如果一个底层函数使用了 this ,而且是以一等公民的方式被调用的,会隐藏各种各样的问题。

var fs = require('fs');
// 太可怕了
fs.readFile('freaky_friday.txt', Db.save);
// 好一点点
fs.readFile('freaky_friday.txt', Db.save.bind(Db));

把 Db 绑定(bind)到它自己身上以后,你就可以随心所欲地调用它的原型链式垃圾代码了。 this 虽然能提高执行速度,但也会带来很多问题,要尽可能地避免使用它,因为在函数式编程中根本用不到它。当然,在使用其他的类库时,你却不得不向这个疯狂的世界低头。

三、 纯函数

3.1 什么是纯函数

**相同的输入永远会得到相同的输出,而且没有任何可观察的副作用。**无论何时调用,调用多少次都不会改变,这就是纯函数。

纯函数就类似于数学中的函数(用来描述输入和输出之间的关系),y=f(x)

纯函数有以下特点:

  • 唯一结果
let xs = [1,2,3,4,5]
// 纯函数
xs.slice(0,3) //[1,2,3]
xs.slice(0,3) //[1,2,3]
xs.slice(0,3) //[1,2,3]
// 不纯函数
xs.splice(0,3) //[1,2,3]
xs.splice(0,3) //[4,5]
xs.splice(0,3) //[]

在函数式编程中,是非常讨厌这种会改变数据的笨函数。我们追求的是那种可靠的,每次都能返回同样结果的函数而不是像 splice 这样每次调用后都把数据弄得一团糟的函数

  • 不接受外部状态
var min = 21;

// 不纯函数
var ckeck = function(age){
    return age >= min;
}

// 纯函数
var check = function(age){
    var min = 21;
    return age >= min;
}
  • 副作用

    在纯函数定义中提到的万分邪恶的副作用到底是什么?,“作用”我们可以理解为一切除结果计算之外发生的
    事情。“作用”本身并没什么坏处, 但是副作用中的“副”是确滋生 bug 的温床,副作用可能包含,但不限于:

    • 更改文件系统

    • 往数据库插入记录

    • 发送一个 http 请求

    • 可变数据

    • 打印/log

    • 获取用户输入

    • DOM 查询

    • 访问系统状态

概括来讲,只要是跟函数外部环境发生的交互就都是副作用。函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。

这并不是说,要禁止使用一切副作用,而是说,要让它们在可控的范围内发生, 副作用让一个函数变得不纯是有道理的:从定义上来说,纯函数必须要能够根据相同的输入返回相同的输出;如果函数需要跟外部事物打交道,那么就无法保证这一点了。

3.2 追求纯函数的好处

  • 可缓存

纯函数总能够根据输入来做缓存,来看一个简单的代码实现:

var memoize = function(f) {
	var cache = {};
	return function() {
		var arg_str = JSON.stringify(arguments);
		cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
		return cache[arg_str];
	};
};


var squareNumber = memoize(function(x){ return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // 从缓存中读取输入值为 4 的结果
//=> 16
squareNumber(5);
//=> 25
squareNumber(5); // 从缓存中读取输入值为 5 的结果
//=> 25

可以通过延迟执行的方式把不纯的函数转换为纯函数:

var pureHttpCall = memoize(function(url, params){
	return function() { return $.getJSON(url, params); }
});

这里有趣的地方在于我们并没有真正发送 http 请求——只是返回了一个函数,当调用它的时候才会发请求。这个函数之所以有资格成为纯函数,是因为它总是会根据相同的输入返回相同的输出:给定了 url 和 params 之后,它就只会返回同一个发送 http 请求的函数。

  • 可移植性/自文档化

纯函数是完全自给自足的,它需要的所有东西都能轻易获得。首先,纯函数的依赖很明确,因此更易于观察和理解——没有偷偷摸摸的小动作。

// 不纯的
var signUp = function(attrs) {
	var user = saveUser(attrs);
	welcomeUser(user);
};
var saveUser = function(attrs) {
	var user = Db.save(attrs);
	...
};
var welcomeUser = function(user) {
	Email(user, ...);
	...
};
// 纯的
var signUp = function(Db, Email, attrs) {
	return function() {
		var user = saveUser(Db, attrs);
		welcomeUser(Email, user);
	};
};
var saveUser = function(Db, attrs) {
	...
	};
var welcomeUser = function(Email, user) {
	...
}

这个例子表明,纯函数对于其依赖必须要诚实,这样我们就能知道它的目的。仅从纯函数版本的 signUp 的签名就可以看出,它将要用到 Db 、 Email 和 attrs ,这在最小程度上给了我们足够多的信息,相比不纯的函数,纯函数能够提供多得多的信息;前者天知道它们暗地里都干了些什么。

在 JavaScript 的设定中,可移植性可以意味着把函数序列化(serializing)并通过socket 发送。也可以意味着代码能够在 web workers 中运行。可移植性是一个非常强大的特性。

命令式编程中“典型”的方法和过程都深深地根植于它们所在的环境中,通过状态、依赖和有效作用(available effects)达成;纯函数与此相反,它与环境无关,只要我们愿意,可以在任何地方运行它。

面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩...以及整个丛林

  • 可测试性

    纯函数让测试更加容易。我们不需要伪造一个“真实的”运行环境,或者每一次测试之前都要配置、之后都要言状态(assert the state)。只需简单地给函数一个输入,然后断言输出就好了。

  • 合理性

使用纯函数最大的好处是引用透明性, 由于纯函数总是能够根据相同的输入返回相同的输出,所以它们就能够保证总是返回同一个结果,这也就保证了引用透明性

四、函数柯里化

curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

var add = function(x) {
	return function(y) {
		return x + y;
	};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12

f 和 g 都是函数, x 是在它们之间通过“管道”传输的值。可以一次性地调用 curry 函数,也可以每次只传一个参数分多次调用,现在使用lodash库来创建一些curry函数

var curry = require('lodash').curry;
var match = curry(function(what, str) {
	return str.match(what);
});
var replace = curry(function(what, replacement, str) {
	return str.replace(what, replacement);
});
var filter = curry(function(f, ary) {
	return ary.filter(f);
});
var map = curry(function(f, ary) {
	return ary.map(f);
});

上面的代码中遵循的是一种简单,同时也非常重要的模式。即策略性地把要操作的数据(String, Array)放到最后一个参数里。到使用它们的时候你就明白这样做的原因是什么了。


match(/\s+/g)("hello world");
// [ ' ' ]

var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }
hasSpaces("hello world");
// [ ' ' ]
hasSpaces("spaceless");
// null
filter(hasSpaces, ["tori_spelling", "tori amos"]);
// ["tori amos"]
var findSpaces = filter(hasSpaces);
// function(xs) { return xs.filter(function(x) { return x.match(
/\s+/g) }) }
findSpaces(["tori_spelling", "tori amos"]);
// ["tori amos"]
var noVowels = replace(/[aeiou]/ig);
// function(replacement, x) { return x.replace(/[aeiou]/ig, repl
acement) }
var censored = noVowels("*");
// function(x) { return x.replace(/[aeiou]/ig, "*") }
censored("Chocolate Rain");
// 'Ch*c*l*t* R**n'

五、代码组合

代码组合又叫函数组合(compose)

// a(b(c(d))) => compose(a,b,c)(d)
var compose = function(f,g) {
	return function(x) {
		return f(g(x));
	};
};

f 和 g 都是函数, x 是在它们之间通过“管道”传输的值。

组合 看起来像是在饲养函数。你就是饲养员,选择两个有特点又遭你喜欢的函数,让它们结合,产下一个崭新的函数。组合的用法如下:

var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);

shout("send in the clowns");

在 compose 的定义中, g 将先于 f 执行,因此就创建了一个从右到左的数据流。这样做的可读性远远高于嵌套一大堆的函数调用,如果不用组合, shout 函数将会是这样的:

var shout = function(x){
	return exclaim(toUpperCase(x));
};

让代码从右向左运行,而不是由内而外运行, 来看一个顺序很重要的例子:

//reverse 反转列表, head 取列表中的第一个元素;所以结果就是得到了一个last 函数
var head = function(x) { return x[0]; };
var reverse = reduce(function(acc, x){ 
    return [x].concat(acc); 
}, []);
var last = compose(head, reverse);
last(['jumpkick', 'roundhouse', 'uppercut']);
//=>

这个组合中函数的执行顺序就很重要

组合的概念直接来自于数学课本。实际上,现在是时候去看看所有的组合都有的一个特性了。

// 结合律(associativity)
var associative = compose(f, compose(g, h)) == compose(compose(f, g), h);

这个特性就是结合律,符合结合律意味着不管你是把 g 和 h 分到一组,还是把f 和 g 分到一组都不重要。所以,如果我们想把字符串变为大写,可以这么写:

compose(toUpperCase, compose(head, reverse));
// 或者
compose(compose(toUpperCase, head), reverse);

因为如何为 compose 的调用分组不重要,所以结果都是一样的。这也让我们有能力写一个可变的组合(variadic compose)

// 前面的例子中我们必须要写两个组合才行,但既然组合是符合结合律的,我们就可以只写一个,
// 而且想传给它多少个函数就传给它多少个,然后让它自己决定如何分组。
function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}


var lastUpper = compose(toUpperCase, head, reverse);
lastUpper(['jumpkick', 'roundhouse', 'uppercut']);
//=> 'UPPERCUT'
var loudLastUpper = compose(exclaim, toUpperCase, head, reverse)
loudLastUpper(['jumpkick', 'roundhouse', 'uppercut']);
//=> 'UPPERCUT!'

结合律的一大好处是任何一个函数分组都可以被拆开来,然后再以它们自己的组合方式打包在一起。让我们来重构重构前面的例子:

var loudLastUpper = compose(exclaim, toUpperCase, head, reverse);
// 或
var last = compose(head, reverse);
var loudLastUpper = compose(exclaim, toUpperCase, last);
// 或
var last = compose(head, reverse);
var angry = compose(exclaim, toUpperCase);
var loudLastUpper = compose(angry, last);
// 更多变种...

pointfree模式提倡函数无须提及将要操作的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协作起来非常有助于实现这种模式。

// 非 pointfree,因为提到了数据:word
var snakeCase = function (word) {
	return word.toLowerCase().replace(/\s+/ig, '_');
};
// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值