转眼之间已入五月,自己毕业也马上有三年了。大学计算机系的同学大多都在北京混迹,大家为了升职加薪,娶媳妇买房,熬夜加班跟上线,出差pk脑残客户。同学聚会时有不少兄弟已经体重飙升,开始关注13号地铁线上铺天盖地的植发广告。都说25岁是一个男人的分界线,之前是越活越精致,往后是越活越糙。现在体会到了。父母开始老去,自己尚一无所有,攒的钱不够买一平米的房。昨天和一哥们撸串,我问:有啥打算?哥们吞了几口羊肉串,喝了一口啤酒,说:存点钱吧,然后回家。
说实话,我以前的想法也一样。奈何来北京容易,想走却很难。清明节回太原一趟,总觉的路上太过于寂静,大家走路速度太慢,商店关门太早,竟然有些许不适应。兀的发觉,北京肉体虽然每天很疲惫,但灵魂力量却修炼的很强。回到昌平的20平出租屋内,内心暗想,继续混,混到混不下去为止。
之前写过一篇博文《我在百度做外包》,没想到许多同学深有同感。北京的外包群体是一个很大的社会组织结构,他们需要得到更多的关注。但生存是社会永恒的主题,外包人员更要懂得,一切以实力说话,没有技术就没有发言权。没有名校背景的同学进大厂很难,反过来说外包也是另外一条路,只不过这条路需要付出更多的努力,机会永远留给有准备的人。
这篇博文,作为我侥幸从外包转成正式百度员工的纪念。
这是一个系列博客,希望他能在我的码农生涯中留下些什么。
闲话不多说,这篇文章主要和大家分析下前端的函数式编程思想。纲要如下:
函数式编程思维
函数式编程常用核心概念
当下函数式编程最热的库
函数式编程的实际应用场景
一 函数式编程思维
范畴论 Category Theory
对于前端,所有的成员是一个集合,变形关系是函数。
函数式编程基础理论
1..函数式编程(Functional Programming)其实相对于计算机的历史而言是一个非常古老的概念,甚至早于第一台计算机的诞生。函数式编程的基础模型来源于 λ (Lambda x=>x*2)演算,而 λ 演算并非设计于在计算机上执行,它是在 20 世纪三十年代引入的一套用于研究函数定义、函数应用和递归的形式系统。
2.函数式编程不是用函数来编程,也不是传统的面向过程编程。主旨在于将复杂的函数符合成简单的函数(计算理论,或者递归论,或者拉姆达演算)。运算过程尽量写成一系列嵌套的函数调用
3.JavaScript 是披着 C 外衣的 Lisp。
4.真正的火热是随着React的高阶函数而逐步升温。
5.函数是一等公民。所谓”第一等公民”(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
6.不可改变量。在函数式编程中,我们通常理解的变量在函数式编程中也被函数代替了:在函数式编程中变量仅仅代表某个表达式。这里所说的’变量’是不能被修改的。所有的变量只能被赋一次初值。
7.map & reduce他们是最常用的函数式编程的方法。
将上面的概念简述一下:
1. 函数是”第一等公民”
2. 只用”表达式",不用"语句"
3. 没有”副作用"
4. 不修改状态
5. 引用透明(函数运行只靠参数)
二 函数式编程常用核心概念
•纯函数
•函数的柯里化
•函数组合
•Point Free
•声明式与命令式代码
•核心概念
1.纯函数
什么是纯函数呢?
对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态的函数,叫做纯函数。
举个栗子:
var xs = [1,2,3,4,5];// Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
xs.slice(0,3);
xs.slice(0,3);
xs.splice(0,3);// Array.splice会对原array造成影响,所以不纯
xs.splice(0,3);
2.函数柯里化
传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
我们有这样一个函数checkage:
var min = 18;
var checkage = age => age > min;
这个函数并不纯,checkage 不仅取决于 age还有外部依赖的变量 min。 纯的 checkage 把关键数字 18 硬编码在函数内部,扩展性比较差,柯里化优雅的函数式解决。
var checkage = min => (age => age > min); var checkage18 = checkage(18); // 先将18作为参数,去调用此函数,返回一个函数age => age > 18;
checkage18(20);// 第二步,上面返回的函数去处理剩下的参数,即 20 => 20 > 18; return true;
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
return function (x) {
return x + y;
};
}
addX(2)(1) // 3
3.函数组合
为了解决函数嵌套过深,洋葱代码:h(g(f(x))),我们需要用到“函数组合”,我们一起来用柯里化来改他,让多个函数像拼积木一样。
const compose = (f, g) => (x => f(g(x)));
var first = arr => arr[0];
var reverse = arr => arr.reverse();
var last = compose(first, reverse);
last([1, 2, 3, 4, 5]); // 5
函数组合交换律,类似于乘法交换律:
4.Point Free
const f = str => str.toUpperCase().split(' ');
var toUpperCase = word => word.toUpperCase();
var split = x => (str => str.split(x));
var f = compose(split(' '), toUpperCase);
f("abcd efgh");
把一些对象自带的方法转化成纯函数,然后通过函数组合去调用,这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。是不是很方便!
5.声明式与命令式代码
在我们日常业务开发中,写的代码绝大多数都为命令式代码;
//命令式
let CEOs = [];
for (var i = 0; i < companies.length; i++) {
CEOs.push(companies[i].CEO)
}
//声明式
let CEOs = companies.map(c => c.CEO);
6.核心概念
下面我们再深入一下,大家注意好好理解吸收:
高阶函数
高阶函数,就是把函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。
//命令式
var add = function (a, b) {
return a + b;
}; function math(func, array) {
return func(array[0], array[1]);
}
math(add, [1, 2]); // 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);
} //ES6强制使用尾递归
我们看一下递归和尾递归执行过程:
递归:
function sum(n) {
if (n === 1) return 1;
return n + sum(n - 1);
}
sum(5)
(5 + sum(4))
(5 + (4 + sum(3)))
(5 + (4 + (3 + sum(2))))
(5 + (4 + (3 + (2 + sum(1)))))
(5 + (4 + (3 + (2 + 1))))
(5 + (4 + (3 + 3)))
(5 + (4 + 6))
(5 + 10)
15 // 递归非常消耗内存,因为需要同时保存很多的调用帧,这样,就很容易发生“栈溢出”
尾递归
function sum(x, total) {
if (x === 1) {
return x + total;
}
return sum(x - 1, x + total);
}
sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14)
15
范畴与容器
1.函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。
2.函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
容器与函子(Functor)
$(...) 返回的对象并不是一个原生的 DOM 对象,而是对于原生对象的一种封装,这在某种意义上就是一个“容器”(但它并不函数式)。
Functor(函子)遵守一些特定规则的容器类型。任何具有map方法的数据结构,都可以当作函子的实现。
Functor 是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力。把东西装进一个容器,只留出一个接口 map 给容器外的函数,map 一个函数时,我们让容器自己来运行这个函数,这样容器就可以*地选择何时何地如何操作这个函数,以致于拥有惰性求值、错误处理、异步调用等等非常牛掰的特性。
下面我们看下函子的代码实现:
var Container = function (x) {
this.__value = x;
}
// 函数式编程一般约定,函子有一个of方法
Container.of = x => new Container(x);
// Container.of(‘abcd’);
// 一般约定,函子的标志就是容器具有map方法。该方法将容器
// 里面的每一个值, 映射到另一个容器。
Container.prototype.map = function (f) {
return Container.of(f(this.__value))
}
Container.of(3)
.map(x => x + 1) //=> Container(4)
.map(x => 'Result is ' + x); //=> Container('Result is 4')
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
}
(new Functor(2)).map(function (two) {
return two + 2;
});
// Functor(4)
Functor.of = function (val) {
return new Functor(val);
};
Functor.of(2).map(function (two) {
return two + 2;
});
// Functor(4)
下面我们介绍一些常用的函子。
Maybe 函子
var Maybe = function (x) {
this.__value = x;
}
Maybe.of = function (x) {
return new Maybe(x);
}
Maybe.prototype.map = function (f) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}
Maybe.prototype.isNothing = function () {
return (this.__value === null || this.__value === undefined);
}
//新的容器我们称之为 Maybe(原型来自于Haskell,Haskell是通用函数式编程语言)
Functor.of(null).map(function (s) {
return s.toUpperCase();
});
// TypeError Maybe.of(null).map(function (s) {
return s.toUpperCase();
});
// Maybe(null)
错误处理、Either函子
我们的容器能做的事情太少了,try/catch/throw 并不是“纯”的,因为它从外部接管了我们的函数,并且在这个函数出错时抛弃了它的返回值。Promise 是可以调用 catch 来集中处理错误的。事实上 Either 并不只是用来做错误处理的,它表示了逻辑或。
条件运算if...else是最常见的运算之一,函数式编程里面,使用 Either 函子表达。Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。
class Either extends Functor {
constructor(left, right) {
this.left = left;
this.right = right;
}
map(f) {
return this.right ?
Either.of(this.left, f(this.right)) :
Either.of(f(this.left), this.right);
}
}
Either.of = function (left, right) {
return new Either(left, right);
};
使用Either函子:
var addOne = function (x) {
return x + 1;
};
Either.of(5, 6).map(addOne);
// Either(5, 7);
Either.of(1, null).map(addOne);
// Either(2, null);
Either
.of({
address: 'xxx'
}, currentUser.address)
.map(updateField);
AP函子
class Ap extends Functor {
ap(F) {
return Ap.of(this.val(F.val));
}
}
function addOne(x) {
return x + 1;
}
Ap.of(addOne).ap(Functor.of(1)) // ap函子,让addOne可以用后面函子中的val运算 结果为Ap(2)
IO函子
class IO extends Monad {
map(f) {
return IO.of(compose(f, this.__value))
}
}
在这里,我们提到了Monad,Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。Promise 就是一种 Monad。Monad 让我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如IO和其它异步任务。
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {
return this.map(f).join();
}
}
关于更多的Monad介绍,可以移步知乎什么是Monad。
三 流行的几大函数式编程库
1.Rxjs
var clicks = Rx.Observable
.fromEvent(document, 'click')
.bufferCount(2)
.subscribe(x => console.log(x)); // 打印出前2次点击事件
2.Cycle.js
function main(sources) {
const sinks = {
DOM: sources.DOM.select('input').events('click')
.map(ev => ev.target.checked)
.startWith(false)
.map(toggled =>
<div>
<input type="checkbox" /> Toggle me
<p>{toggled ? 'ON' : 'off'}</p>
</div>
)
};
return sinks;
}
const drivers = {
DOM: makeDOMDriver('#app')
};
run(main, drivers);
3.Underscore.js
4.Lodash.js
var abc = function (a, b, c) {
return [a, b, c];
};
var curried = _.curry(abc);
curried(1)(2)(3);
function square(n) {
return n * n;
}
var addSquare = _.flowRight(square, _.add); // 类似于上面说的函数组合
addSquare(1, 2);
// => 9