函数式编程-笔记
函数式编程(简写FP) 是编程范式之一,我们常听说的编程范式还有面向过程编程,面向对象编程
函数式编程式是随着React的流行受到越来越多的关注
Vue 3 也开始拥抱函数式编程
函数式编程可以抛弃this
打包过程中可以更好的利用 tree shaking
过滤无用代码
方便测试,方便并行处理
有很多库可以帮助我们进行函数式开发:lodash
underscore
ramda
总结
:
- 函数式编程是一种编程范式,跟面向对象编程式并列关系
- 函数式编程中的函数实际上指的就是数学中的函数
- 可以很大程度上让代码可以重用
- 但函数式编程并不能提高程序的性能,因为大量的使用闭包在某种程度上来说还会降低性能(占用内存)
高阶函数
使用高阶函数的意义
- 抽象可以帮我们屏蔽细节,只需要关于与我们的目标
比如,如果我在多个场景中都用到了同样的一个需要对数组或对象或者其他类型的值去做一些处理,那么为了代码的更简洁,也为了易用
我们就可以去封装一个通用的方法,来去操作,不关注本身,只关注结果的实现,比如循环,过滤,查找目标等等。 - 高阶函数是用来解决抽象通用的问题
我们在日常工作中经常会用到的高阶函数 forEach map filter every some find/findIndex reduce sort
…
那么针对于这些常用函数方法内部是如何处理的,就简单地模拟下:
// forEach
const forEach = (array,fn)=> {
for (let i =0; i<array.length; i++) {
fn(array[i],i)
}
}
let arr = ["tiantian","mingming","cici","xiongxiong","nuannuan"]
forEach(arr,(item,index)=>{
console.log(item,index)
})
// map
const map = (array,fn) => {
let results = []
for (let [index,item] of array.entries()) {
if (fn(item,index)) {
results.push(fn(item,index))
}
}
return results
}
let newArr = map(arr, (item, index) => {
return index
})
console.log(newArr)
// filter
const filter = (array, fn) => {
let result = [];
for (let i = 0, length = array.length; i < length; i++) {
if (fn(array[i], i)) {
result.push(array[i], i);
}
}
return result;
}
let arr = ["tiantian", "mingming", "cici", "xiongxiong", "nuannuan"];
let filters = filter(arr, (item, index) => {
return item != "tiantian";
});
console.log(filters);
// every
const every = (array, fn) => {
let result = true
for (let [index, value] of array.entries()) {
result = fn(value, index)
if (!result) {
break
}
}
return result
}
// some
const some = (array, fn) => {
let result = false
for (let [index, value] of array) {
result = fn(value, index)
if (result) {
break
}
}
return result
}
......
......
......
函数式一等公民,为什么这么说呢?
原因有三点:
- 函数可以存储在变量中
- 函数可以作为参数
- 函数可以作为返回值
柯里化函数的理解:
- 柯里化可以让我们给一个函数传递较少的参数并得到一个已经记住了某些固定参数的新函数
- 内部通过闭包实现了对函数参数的缓存
- 让函数变得更加灵活,从而使函数的粒度更小
- 总而言之,函数柯里化可以使多元函数转换成一元函数,可以组合使用函数产生强大的功能
柯里化原理模拟,实现lodash
中的curry
方法
// 模拟实现 lodash 中的 curry方法
function getSum(a, b, c) {
return a + b + c;
}
const curried = curry(getSum);
function curry(func) {
return function curriedFn(...args) {
console.log(func.length, "函数的个数");
// 判断实参和形参的个数
// 如果传递的参数个数 小于 实际要传递的参数个数的话,
// 那么就返回一个新的函数,内部依次递归,直到参数调用完毕为止
if (args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)));
};
}
return func(...args);
};
}
console.log(curried(1)(2)(3));
函数组合的说法:
- 其实函数可以看做是一个处理数据的管道,管道中输入参数x,在管道中对数据处理后得到y
- 如果一个函数比较复杂,这个管道比较长,这时候我们就可以使用到函数组合来实现
- 其实就是把这个管道分成好几个短的管道,比如A,B,C这三个管道(函数)
- 那么函数组合的使用就得需要满足结合律,就是多个函数最终都是针对于一个数据去做处理
- 函数组合的条件,这多个函数都必须是一元函数
- 函数组合就是可以把多个一元函数组合成一个功能更强大的函数,默认从右到左去执行
函数组合我们可以使用lodash
中的fp
模块中flowRight
方法来去实现,也可以用folktale
库中的compose
方法实现函数组合,下面来模拟下函数组合的方法
// 模拟 lodash 中的 flowRight
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
// function compose (...args) {
// return function (value) {
// return args.reverse().reduce(function (acc, fn) {
// return fn(acc)
// }, value)
// }
// }
// 使用Es6箭头函数简写compose
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
const f = compose(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))
纯函数的理解:
- 纯函数对于相同的输入永远会得到相同的输出
- 纯函数中的函数指的就是数学中的函数
- 副作用会让一个函数变得不纯,副作用在我们平时编码的时候是不可能避免的,因为代码难免会依赖外部的配置文件,数据库等等,
- 我们只能最大程度上去重置副作用在可控的范围内发生
如何尽量避免副作用的出现?
我们可以去实现一个容器,把所有不纯的操作都封装到这个容器内,交给调用者来执行处理,这是不是很像我们平时工作当中的甩锅现象,那么怎么实现呢,folktale
库内部提供了函子来帮我们解决这个事情
函子的理解:
- 函子是一个特殊的容器(对象),这个容器内部封装了一个值,通过map传递一个函数来对值进行处理
- MayBe 函子的作用是处理外部的控制情况,防止空值的异常
- IO 函子内部封装的值是一个函数,把不纯的操作都封装到这个函数中,最终在执行的时候把不纯的操作都交给调用者去处理
- Monad 函子内部封装的是一个函数(这个函数返回函子),目的是通过join方法来避免函子嵌套
函子-Functor
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(null).map((x) => x.toUpperCase());
console.log(r);
// 副作用:不能对于空值去做处理
函子-MayBe
class Maybe {
static of(value) {
return new Maybe(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this._value));
}
isNothing() {
return this._value === null || this._value === undefined;
}
}
let r = Maybe.of(undefined).map((x) => x.toUpperCase());
console.log(r);
// 针对于空值的异常去做处理,如果传入的值是null或者undefined 那么调用map方法的时候就给个空值 null,否则正常调用map中的回调函数去对值做处理
// 副作用:虽然Maybe函子可以去处理空值的异常,但在链式调用多次map的时候,它哪一次出现了空值,我们是不太明确的
函子-Either
// Either 两者中的任何一个,类似于 if...else...的处理
// 异常会让函数变得不纯,而Either函子可以用来做异常处理
// 因为Either函子可以定义两种类型,我们可以二选一,一个Left 一个Right
class Left {
static of(value) {
return new Left(value);
}
constructor(value) {
this._value = value;
}
map() {
return this;
}
}
class Right {
static of(value) {
return new Right(value);
}
constructor(value) {
this._value = value;
}
map(fn) {
return Right.of(fn(this._value));
}
}
function parseJSON(str) {
try {
return Right.of(JSON.parse(str));
} catch (err) {
return Left.of({ error: err.message });
}
}
let r = parseJSON("{name: 25}");
console.log(r);
函子-IO
// IO 跟其他函子不同的是: 函子中的_value 是一个函数,这里是把函数作为值来处理
// IO 函子可以把不纯的操作存储到_value中,我们把不纯的操作延迟到调用的时候执行,也就是惰性执行,从而使当前的操作是一个纯的操作
// 有了IO函子,我们就可以把各种不纯的操作装进笼子里,但是这些不纯的操作最终都是要执行的
// 所以我们可以把不纯的操作交给调用者处理,这其实很像我们工作时候的甩锅现象
const fp = require("lodash/fp");
class IO {
static of(value) {
return new IO(() => value);
}
constructor(fn) {
this._value = fn;
}
map(fn) {
return new IO(fp.flowRight(fn, this._value));
}
}
let r = IO.of(process).map((p) => p.execPath);
console.log(r._value()); //如果有不纯的操作,延迟到调用_value()
函子-Monad
/*
Monad 函子是可以变扁的Pointed 函子,IO(IO(x))
一个函子如果具有join和of两个方法并遵守一些定律就是一个 Monad
什么时候会使用到Monad呢?
当一个函数返回的是一个函子
这个时候就应该想到使用Monad
Monad可以帮我们解决函子嵌套的问题
*/
const fs = require("fs");
const { map } = require("lodash");
const fp = require("lodash/fp");
class IO {
static of(value) {
return new IO((value) => value);
}
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 = (filename) => new IO(() => fs.readFileSync(filename, "utf-8"));
let print = (x) =>
new IO(() => {
console.log(x);
return x;
});
let r = readFile("package.json").flatMap(print).join();
console.log(r);