目录
导读
- 最近杠上的是函数式编程,简称FP。最开始对它的接触大概是一年前左右,那是公司大牛做的一次技术分享。听的过程中是这样的感觉: 我X,还有这种操作,听完以后:我是谁,我在哪,我在干什么,最后仅仅模糊的记得一个概念:函数柯里化。由此我的脑子里稀里糊涂的多了一个概念:函数柯里化就是函数式编程。
- 随着这次对它系统的学习,才发现之前对FP的理解实在是太狭隘了,太狭隘了,太狭隘了…。下面就对自己的学习成果做下记录,后续也会随着自己的理解加深不断的纠错与补充。
阅读目标
- 理解函数式编程的概念
- 了解函数式编程使用的一些基本概念
- 认识并使用几种函子
什么是函数式编程
1. 函数式编程是一种开发范式
- 常见开发范式有两种:面向过程和面向对象,而函数式编程就是将要接触的第三种开发范式。
- 在学习函数式编程时,要和面向过程对比着学,找到类似于刚从面向过程开发转到面向对象开发的那种感觉
- 函数式编程和面向对象编程在某些地方有些类似,在学习的过程中脑袋里经常会有这样的疑问:这跟面向对象差不多嘛。此时你要做的就是忽略这种想法,这就是我提到第二点的原因。
2. 函数式编程起源于数学
- 准确的说起源于数学中的一个分支:范畴学。我将它简单的归纳为:研究两个集合之间的关系。它的运算方法就是函数式编程,而这个方法正好可以用来学代码。
- 函数式编程的起源决定了它的一个基调:使用到的函数必须是纯函数。因为数学本身就是一门不允许模棱两可的学问。
- 如果对范畴学有兴趣,可以多多研究,可以加深对函数式编程的理解,反之,就多注意一下函数式编程的特点,同样也可以无压力的使用它,虽然我还没有做到。
函数式编程的基本理论
1. 函数是一等公民
我对他的理解有两部分:
- 每个函数要满足单一责任、最小意外等原则
- 函数可以跟其他类型的数据一样,当做参数传递,赋值给变量,存放到数组中…
2. 使用到的函数必须是纯函数
- 纯函数的定义:相同的输入,永远会得到相同的输出,而且没有可观察的副作用
- 副作用的定义:在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。它可能包含但不限于如下操作:
更改文件系统-往数据库插入记录-发送一个http请求-可变数据-打印log-获取用户输入-DOM查询-访问系统状态
3. 追求纯函数的理由
可缓存-移植性强-可测试性-引用透明
- 原来想对每个理由展开说明,后来感觉这些基本都见名知意,而且这也不是特别需要注意的内容,就不想占用过多篇幅,有问题留言即可
函数式编程的两个核心函数
如果要使用函数式编程我们需要借助一些第三方库,比如
lodash(普通版本和FP版本)
和ramda
,后续所有的示例都是基于FB lodash
1. curry函数-用于将指定函数柯里化
-
函数柯里化:可以跟普通函数一样直接调用,也可只传递给函数传递一部分参数,让它返回一个函数去处理剩下的参数
// 正常函数 let add = function(x, y) { return x+y; } let result = add(1+1); // 2 // 柯里化后的函数 let curryAdd = _.curry(add); let addOne = curryAdd(1); // Function let addTen = curryAdd(10); // Function let result1 = addOne(1) // 2 let result2 = addTen(1) // 11 let result3 = curryAdd(1, 20) // 21
至于原理,就是
_.curry
函数将我们add函数转化为如下类型的函数:// 通过闭包的方式保留参数x var add = function(x) { return function(y) { return x + y; }; };
-
FP lodash除了提供
_.curry
函数帮助将自定义的函数转化成柯里化函数,FB lodash还将一些普通lodash版本中的函数封装成柯里化供我们使用,比如_.add/_.head/_.first
等等,下面我们查看几个列子,更多查看FB lodash// 使用FB lodash中_.add直接实现addOne和addTen const addOne = _.add(1); // Function const addTen = _.add(10); // Function const result1 = addOne(1); // 2 const result2 = addTen(10); // 20 // 使用FB lodash中_.head获取数组的第一个元素 const result3 = _.head([2,34,8); // 2 const result4 = _.head('abcd); // a // _.heade的实现原理 const getElementByIndex = function(index, arr) { // head原方法 return arr[index]; } const curryHead = _.curry(getElementByIndex); const getFirtElement = curryHead(0); getFirtElement([3,4,8]); // 3
-
在柯里化的函数中,参数顺序也是有讲究的,仔细观察上面的这些柯里化后的函数,会发现我们都将要操作的数据放到了最后一个参数里,现在只需注意这点就行,后续就会慢慢明白为什么这样做了
2. compose函数-组合函数
- 概念:将传入的函数组合起来,返回一个从右到左执行的管道函数
简易源码如下:let toUpperCase = function(x) { return x.toUpperCase(); }; let exclaim = function(x) { return x + '!'; }; let sayHi = function(x) { return 'Hi,' + x ; }; let shout = _.compose(sayHi, toUpperCase, exclaim); shout('We are handsome'); // Hi,WE ARE HANDSOME!
// f和g都是函数,x是组合后形成函数的需传参数 var compose = function(f,g) { return function(x) { // compose函数执行顺序都是从右到左 return f(g(x)); }; };
- compose函数其实就是帮助我们创建了一个从左到右的数据流,再加上每一步都是纯函数,大大增强了代码的可读性
- 所有的compose都遵循一个规律:数学中的结合律。它可以让我们的组合更加灵活,而且肯定不会影响结果,举个栗子:
let toUpperCase = function(x) { return x.toUpperCase(); }; let exclaim = function(x) { return x + '!'; }; let sayHi = function(x) { return 'Hi,' + x ; }; // toUpperCase/exclaim/sayHi 三个函数只要保证顺序不变,随意我们组合,比如 _.compose(sayHi, toUpperCase, exclaim) ==> _.compose(_.compose(sayHi, toUpperCase), exclaim) ==> _.compose(sayHi, _.compose(toUpperCase, exclaim));
使用函数式编程做个小栗子
需求描述:请求接口,将接口中的图片都渲染到页面中
1. 面向过程编程示例
function getDataAppendBody(word) {
$.getJSON('https://api.flickr.com/services/feeds/photos_public.gne?tags=' + word + '&format=json&jsoncallback=?', (data) => {
console.log(data);
$('body').html(data.items.map((item) => {
return $('<img />', { src: item.media.m });
}));
});
}
getDataAppendBody('dogs');
2. 函数式编程示例
/********************* 准备工作 ***************************/
// 强调: getJSON和setHtml都是柯里化函数
const Impure = {
getJSON: _.curry(function(callback, url) {
$.getJSON(url, callback);
}),
setHtml: _.curry(function(sel, html) {
$(sel).html(html);
})
};
const img = function (url) {
return $('<img />', { src: url });
};
const trace = _.curry(function(tag, x) {
console.log(tag, x);
return x;
});
const url = function (word) {
return 'https://api.flickr.com/services/feeds/photos_public.gne?tags=' + word + '&format=json&jsoncallback=?';
};
/********************* 开始操作 ***************************/
// _.map和_.prop 建议先
var images = _.compose(_.map(img),_.map(_.compose(_.prop('m'), _.prop('media'))), _.prop('items'));
var renderImages = _.compose(Impure.setHtml("body"), images);
var app = _.compose(Impure.getJSON(renderImages), url);
app("cats");
3. 总结
- 函数式编程开发前,会将所有的步骤都处理成纯函数,而且这些纯函数的移植性都特别强,其他业务也都可以用。麻烦一次,永久方便,更爽的是可以拿这些纯函数,用_.compose随意组合,裂变出更多功能的函数,这点是面向对象做不到的
- 但从代码量上看,其实面向过程完爆函数式编程,但从长远考虑,函数式编程留下了更强的扩展性,可读性也更加强(这一点等写习惯了才会慢慢了解)。
- 在上面的栗子中有两个问题急需我们解决:如何判空和如何捕捉异步的error,这就涉及到我们之后要接触的两个函子
Maybe
和Either
不可或缺的函子(functor)
1. 啥是函子
functor 是实现了 map 函数并遵守一些特定规则的容器类型。 如下是一个最为基础的函子
const Container = function(val) {
this.__value = val;
}
Container.of = function(x) {
return new Container(x);
}
Container.prototype.map = function(f) {
return Container.of(f(this.__value))
}
Container.of(2).map(function(two){ return two + 2 })
Container.of("bombs").map(concat(' away')).map(_.prop('length'))
上面提到的规则,总结如下几个:
- 只有一个属性,并且该属性可以是任意类型
- of函数可有可无,它仅仅是用来避免在创建容器时避免忘记写new
- map函数要返回一个新的函子对象,这样我们就可以连续map了
- 函子的原型方法可以有扩展
2. Maybe函子-判空
首先看个栗子,针对如下对象取到name的值
const serverResponce = {
company: { department: { name: 'xxxxx' } },
}
我们使用如下两种方式来取值,
// 正常方式
const name = serverResponce.company.department.name;
// Maybe函子取值
const name = Maybe.of(serverResponce).map(_.prop('company')).map(_.prop('department')).map(_.prop('name'));
分析一波:
- 第一种方式,如果company和department某一项为null或undefine,程序将直接报错。当然可以在每层取值都进行判断,避免程序中断,但成大过大
- 第二种方式,如果出现中间某项为空,最终会返回一个
Maybe.of(null)
,是否异常,只需判断name是否为Maybe.of(null)
即可。
接下来研究一下Maybe函子的实现
const Maybe = function(val) {
this.__value = val;
}
Maybe.of = function(x) {
return new Maybe(x);
}
Maybe.prototype.isNothing = function() {
return (this.__value === null || this.__value === undefine);
}
// 还没有搞清楚怎么数组结构
Maybe.prototype.map = function(f) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value))
}
跟Container主要区别,就是多了一个函数isNothing,接着在每次调用Map的时候优先执行一下this.isNothing()
,如果为空就直接返回一个Maybe.of(null)
3. Either函子
除了Either函子,还有IO/Task/Monad…, 后续可能会在单独开一篇博客详细介绍函子。