函数式编程的概念、特性及具体应用。
概念
函数式编程(Functinal Programing, FP)是相对于面向过程、面向对象编程的范式之一。
面向对象编程思维:把现实世界中的食物抽象成程序世界中的类和对象,通过封装、继承和多台来演示事物事件的联系。
函数式编程思维:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)。
- 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会设计很多有输入输出的函数。
- x > f(联系、映射) > y, y = f(x)
- 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如: y = sin(x),x和y的关系.
- 相同的输入始终要得到相同的输出(纯函数)。
- 函数式编程用来描述数据(函数)之间的映射
// 面向过程
let num1 = 1;
let num2 = 2;
let sum = num1 + num2;
// 函数式
function add(n1, n2) {
return n1 + n2;
}
let sum = add(1, 2);
函数式编程特点: <u>通过细粒度的拆分使得函数复用性更高,再通过组合产生更强大的函数。</u>
在Javascript中的函数式编程: <u>函数作为一等公民,既可以作为参数也可以作为返回值是高阶函数的基础概念。</u>
高阶函数hoc
高阶函数是用来抽象通用的问题,屏蔽实现细节以此达到只需要关注与我们的目标。
let arr = [1,2,3];
// 面向过程
for (let i = 0; i < arr.length; i++) {
// ...
}
// 高级函数
forEach(arr, item => {
// ...
})
// map、filter、reduce等等
闭包closure
概念:嵌套函数引用外部函数作用域的成员造成的引用捆绑从形成闭包。
本质:函数在执行时会放到一个执行栈上当函数执行完毕后从执行栈删除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。
// 函数作为返回值
function makeFn() {
let msg = 'xxx';
return function() {
console.log(msg);
}
}
// once
function once(fn) {
let done = false;
return function() {
if (!done) {
done = true;
return fn.apply(this, arguments);
}
}
}
纯函数
相同输入永远会得到相同的输出,没有任何可观察的副作用
const arr = [1,2,3,4,5];
// 纯函数
console.log(arr.slice(0, 3))
// 非纯函数
console.log(arr.splice(0, 3))
<u>好处:可缓存、可测试、有利于并行处理</u>
// 1.记忆函数:可缓存
function memorize (fn) {
let cache = {};
return function() {
const key = JSON.stringify(arguments);
cache[key] = cache[fn] || fn.apply(fn, arguments);
return cache[key];
}
}
// 2.可测试
// 纯函数使得单元测试更加方便
// 3.并行处理 Web Worker
// 再多线程环境下并行操作共享的内存数据很可能会出现意外情况
// 纯函数不需要访问共享的内存数据,所以并行环境下可以任意运行纯函数
柯里化haskell-brooks-curry
当一个函数由多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变) 然后返回一个新的函数接受剩余的参数,返回结果
function curry (fn) {
return function curriedFn() {
const toArray = arrLike => Array.prototype.slice.call(arrLike);
const innerArgs = toArray(arguments);
// 判断实参和形参的个数
if (innerArgs.length < fn.length) {
return function() {
return curriedFn.apply(null, innerArgs.concat(toArray(arguments)));
}
}
return fn.apply(fn, arguments);
}
}
总结:<u> 通过闭包对函数参数进行缓存、将多元函数转成一元函数</u>
组合compose
纯函数和柯里化很容易写出洋葱代码h(g(f(x)))
函数组合可以让我们把细粒度的函数重新组合成一个新的函数
管道:处理数据输入到输出的中间过程被称为管道
组合:把处理数据的管道(函数)链接起来,让数据穿过多个管道形成最终结果
// 简易版
function compose (f, g) {
return function(param) {
return f(g(param))
}
}
function reverse (arr) {
return arr.reverse();
}
function first (arr) {
return arr[0];
}
const result = compose(first, reverse);
console.log(result([1, 2, 3]));
// 公用版
// ES5
function compose () {
const fns = Array.from(arguments);
return function(param) {
return fns.reduceRight((acc, cur) => {
return cur(acc);
}, param)
}
}
// ES6
const compose = (...fns) => param => fns.reduceRight((acc, cur) => cur(acc), param);
函子functor
函子由以下构成
容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法可以运行一个函数对值进行处理(变形关系)
利用函子可以吧副作用控制在可控的范围内、异常处理、异步操作等
class Container {
static of (value) {
return new Container(value);
}
constructor (value) {
this._value = value;
}
map (fn) {
return Container.of(fn(this._value));
}
}
let r = Container.of(2)
.map(x => x + 3)
.map(x => x * x);
console.log(r);
总结:<u>函数式编程的运算不直接操作值,而是由函子完成,被操作的值为似有变量,只能通过函子暴露出的map方法处理,最终map方法返回一个包含新值的函子。所以不同场景就衍生出不同类型的函子</u>
maybe函子
对外部控制情况做处理的函子(控制副作用在允许的范围)
class Maybe {
static of (value) {
return new Mabe(value);
}
constructor(value) {
this._value = value;
}
map (fn) {
return Maybe.of(this.isNothing() ? null : fn(this._value));
}
isNothing () {
this. this._value === null || this._value === undefined;
}
}
// let r = Maybe.of('hello world')
// .map(x => x.toUpperCase());
// let r = Maybe.of(null)
// .map(x => x.toUpperCase());
// 对于设置null的情况不好排查和调试
let r = Maybe.of('hello world')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '));
either函子
Either 两者中的任何一个,类似于if...else...的处理 异常会让函数变的不纯,Either函子可以用来做异常处理
class Left {
static of (value) {
return new Left(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return this;
}
}
class Right {
static of (value) {
return new Right(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return Right.of(fn(this._value));
}
}
const r1 = Right.of(12).map(x => x + 2);
const r2 = Left.of(12).map(x => x + 2);
console.log(r1, r2); // 14 12
function parseJSON (str) {
try {
return Right.of(JSON.parse(str));
} catch (e) {
return Left.of({ error: e.message })
}
}
// 异常
// let r = parseJSON('{ name: zs }');
// console.log(r);
// 正常
let r = parseJSON('{ "name": "zs" }')
.map(x => x.name.toUpperCase());
console.log(r);
io函子
IO函子中的_value是一个函数,这里是把函数作为值来处理
IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
把不纯的操作交给调用者来处理
const fp = require('lodash/fp');
class IO {
static of (x) {
return new IO(function() {
return x;
})
}
constructor (fn) {
this._value = fn;
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
const r = IO.of({a:1}).map(obj => console.log(obj));
console.log(r);
folktale库重的task异步执行
// 基本用法
const { compose, curry } = require('folktale/core/lamda');
const { toUpper, first } = require('lodash/fp');
let f = curry(2, function (x, y) {
console.log(x + y);
})
f(3, 4);
f(3)(4);
let f = compose(toUpper, first);
f(['one', 'two']);
const fs = require('fs');
const { task } = require('folktale/concurrency/task');
const { split, find } = require('lodash/fp');
function readFile (filename) {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err);
resolver.resolve(data);
})
})
}
readFile('plugins.json')
.map(split('\n'))
.map(find(item => item.includes('version')))
.run()
.listen({
onRejected: err => console.log(err),
onResolved: data => console.log(data)
})
pointer函子
Pointer函子是实现了of静态方法的函子
of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中使用map来处理值)
class Container {
static of (value) {
return new Container(value);
}
// ...
}
Container.of(2).map(x => x + 5);
monad函子
// IO函子嵌套问题
const fp = require('lodash/fp');
class IO {
static of (x) {
return new IO(function() {
return x;
})
}
constructor (fn) {
this._value = fn;
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
let readFile = function (filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8');
})
}
let print = function (x) {
return new IO(function() {
console.log(x);
return x;
})
}
let car = fp.flowRight(print, readFile);
let r = cat('plugins.json')._value()._value();
console.log(r);
Monad函子是可以变扁的Pointed函子, IO(IO(x))
一个函子如果具有join和of两个方法并遵循一些定律就是一个Monad
const fp = require('lodash/fp');
class IO {
static of (x) {
return new IO(function() {
return x;
})
}
constructor (fn) {
this._value = fn;
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
join () {
return this._value();
}
flatMap (fn) {
return this.map(fn).join();
}
}
let readFile = function (filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8');
})
}
let print = function (x) {
return new IO(function() {
console.log(x);
return x;
})
}
let r = cat('plugins.json')
// .map(...)
.flatMap(print)
.join();
console.log(r);
<u>当一个函子返回一个函子时,且操作步骤较多存在嵌套关系时使用Monad函子</u>