什么是函数式编程
基于自己的理解,函数式编程与面向对象编程一样是一种编程思想,它不是用函数编程,主旨在于将复杂的函数合成简单的函数,它与数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。它是随着React高阶函数逐渐火起来的
函数式编程的特点
- 函数是一等公民。所谓“一等公民”,指的是函数与其它类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值
- 不可改变量。在函数式编程中,我们通常理解的变量也被函数给替代了,在函数式编程中变量仅仅代表某个表达式。这里说的变量是不能修改的;所有的变量只能赋一次初值;
- map和reduce是最常用的函数式编程的方法
函数式编程中的核心概念
纯函数:对于相同的输入,永远得到相同的输出,而且没有任何副作用,也不依赖外部环境的状态;纯函数不仅能降低系统的复杂度,还有可缓存的特性(因为同一输入得到同意输出)
eg:
var arr = [1, 2, 3, 4, 5];
// Array.slice是纯函数,没有副作用,对于相同的输入,输出总是固定的
arr.slice(0, 3) // 输出[1, 2, 3] arr没变
arr.slice(0, 3) // 输出[1, 2, 3] arr没变
arr.splice(0, 3) // 输出 [1, 2, 3] arr改变 [4, 5]
arr.splice(0, 3) // 输出 [4, 5] arr改变[]
幂等性:幂等性是指执行无数次后还具有相同的效果,同一的参数运行一次函数应该与连续运行两次结果一致
偏函数:传递给函数一部分参数来调用它,让他返回一个函数去处理剩下的参数
函数柯里化:柯里化通过偏应用函数来实现。通过下面的小栗子来理解
var compareAge = min => (age => age > min);
var compare18 = compareAge(18); // 缓存新函数
commpare18(20);
// curry 前
function add (x, y) {
return x + y;
}
// curry后
var currAdd = function (x) {
return function (y) {
return x + y;
}
}
代码组合(compose):看起来像是在饲养函数。你就是饲养员,选择两个有特点又遭你喜欢的函数,让它们结合,产下一个崭新的函数(引用的_),可以通过函数的组合来解决一些函数的嵌套写法,更加容易阅读
var compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
f 和 g 都是函数,x 是在它们之间通过“管道”传输的值
eg:
var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);
shout("send in the clowns");
Point Free:把一些对象自带的方法转化成纯函数,减少命名中间变量;
// 非 pointfree,因为提到了数据:word
var snakeCase = function (word) {
return word.toLowerCase().replace(/\s+/ig, '_');
};
// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
声明式与命令式代码:命令式代码就是我们通过编写一条又一条指令去让计算机执行一些动作,即告诉计算机怎么去做,例如for循环等。而声名式就要优雅多了,我们通过写表达式的方式来声明我们想干什么,而不是怎么去做;
// 命令式
var makes = [];
for (i = 0; i < cars.length; i++) {
makes.push(cars[i].make);
}
// 声明式
var makes = cars.map(function(car){ return car.make; });
惰性函数:惰性载入表示函数执行的分支只会在函数第一次掉用的时候执行,在第一次调用过程中,该函数会被覆盖为另一个按照合适方式执行的函数,这样任何对原函数的调用就不用再经过执行的分支了。
function createXHR() {
if (typeof XMLHttpRequest != "undefined") {
createXHR = function () {
return new XMLHttpRequest();
}
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined") {
var curxhr;
var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0",
"MSXML2.XMLHttp"];
for (var i = 0, len = versions.length; i < len; i++) {
try {
var xhr = new ActiveXObject(versions[i]);
curxhr = versions[i];
createXHR = function () {
return new ActiveXObject(curxhr);
}
return xhr;
} catch (ex) {
}
}
} else {
throw new Error("No XHR object available.");
}
}
尾调用优化
指函数内部的最后一个动作是函数调用。该调用的返回值直接返回给函数。
函数调用自身,称为递归。如果尾调用自身,称为尾递归。递归需要保存大量的调用记录,很容易发生栈溢出,如果采用尾递归,将递归变为循环,那么只需要保存一个调用记录,这样就不会发生栈溢出错误了。
eg:
// 递归
function facforial(n) {
if (n === 1) return 1;
return n * arguments.callee.call(this, n -1);
}
// 强制使用尾递归
function facforial(n, total) {
if (n === 1) return total;
return facforial(n - 1, n * total);
}
范畴与容器
1、可以将范畴想象成是一个容器,里面包含值(value)和值的变形关系,也就是函数。
2、范畴论使用函数,表达范畴之间的关系。
3、伴随着范畴论的发展,就发展出一整套函数的运算方法。该套方法起初只用于数学计算,后来有人将它在计算机上实现了,就有了现在的“函数式编程”。
4、本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。
5、为什么函数式编程要求函数必须是纯函数,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其它的事,否则就无法满足函数运算法则了。
6、函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转换为另一个范畴。这就涉及到了函子;
7、函子是函数式编程中最重要的数据类型,也是基本的运算单位和功能单位。他首先是一种范畴,也就是说是一个容器,包含了值和变形关系。比较特殊的是,它的变形可以依次作用于每一个值,将当前容器变形成另一个容器。
函子的代码实现
1、 任何具有map方法的数据结构,都可以当作函子;
2、 Functor(函子)遵守一些特定规则的容器类型;
3、Functor是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力,把东西装进一个容器,只留出一个接口map给容器外的函数,map一个函数时,我们让容器自己来运行这个函数,这样容器就可以自由地选择何时何地如何操作这个函数,以致于拥有惰性求值、错误处理、异步调用等等特性
var Container = function(val) {
this.__value = val;
}
// 函数式编程一般约定,函子有一个of方法
Container.of = x => new Container(x);
// 一般约定函子的标志就是容器有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')
Maybe函子(if)
函子接受各种函数,处理容器内部的值,这里就有一个问题,容器内部的值有可能是一个空值(比如null),而外部函数未必会有处理空值的机制,如果传入空值,极有可能出错。
Functor.of(null).map(function(s){
return s.toUpperCase();
}) // typeError
class Maybe extends Functor {
map(f) {
return this.val ? Maybe.of(f(thia.val)) : Maybe.of(null);
}
}
Functor.of(null).map(function (s) {
return s.toUpperCase();
}) // Maybe(null)
还有用来处理错误的Either函子,还有AP函子,详细介绍参考阮大大的博客
IO函子
IO跟前面几个函子不同的地方在于,它的__value是一个函数,它把不纯的操作,比如IO、网络请求、DoM操作,包裹到一个函数内,从而推迟这个操作的执行。IO其实也是惰性求值
var fs = require('fs');
var readFile = function (filename) {
return new IO(function(){
return fs.readSync(filename, 'utf-8'); // 依赖了fs.readSync方法,但是没有执行,所以IO还是纯的
})
}
Monad函子
1、Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤,
2、promise就是一种Monad
3、Monad让我们避开了嵌套的地狱,可以轻松的进行深度嵌套的函数式编程
var fs = require('fs');
var _ = require('lodash');
//基础函子
class Functor {
constructor(val) {
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
}
//Monad 函子
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {
//1.f == 接受一个函数返回的事IO函子
//2.this.val 等于上一步的脏操作
//3.this.map(f) compose(f, this.val) 函数组合 需要手动执行
//4.返回这个组合函数并执行 注意先后的顺序
return this.map(f).join();
}
}
var compose = _.flowRight;
//IO函子用来包裹脏操作
class IO extends Monad {
//val是最初的脏操作
static of (val) {
return new IO(val);
}
map(f) {
return IO.of(compose(f, this.val))
}
}
var readFile = function (filename) {
return IO.of(function () {
return fs.readFileSync(filename, 'utf-8');
});
};
var print = function (x) {
console.log("print");
return IO.of(function () {
return x + "函数式";
});
}
var tail = function (x) {
console.log(x);
return IO.of(function () {
return x+"【tail】";
});
}
const result = readFile('./user.txt')
//flatMap 继续脏操作的链式调用
// .flatMap(print);
.flatMap(print)()
.flatMap(tail)();
console.log(result.val());
// console.log(result().val());