PS:一万三千字长文,建议先 mark 后看,拿去吃灰也不错~
以下正文(首发于 2020年11月19日 02:34)
两年前,当我还在做后端的日子里,我在编程时经常使用的是面向对象编程范式,当时我也有接触函数式编程思想,但总感觉函数式没有面向对象好使。直到我转岗到前端,发现使用面向对象的机会很少,一直是函数来函数去的方法定义,普普通通平平无奇。我也一直以为函数式编程就是这样定义函数用来用去,但后来学习到事情的真相,函数式编程真没想象中那么简单。
在阮一峰大佬的函数式编程的教程日志下,清晰记录他的教程开始日期是2017年2月,距离我现在接触函数式编程相隔了四年时间。我看着这个时间特别感慨,大佬都是走在知识前沿,如果我在编程刚开始时候也紧跟大佬脚步,是不是就不会落后太多,一步晚步步晚。于是为了紧跟大佬脚步,我们还是要卷起来咳咳…就这样,我也不写更多的了,有疑问或者犹豫的随时评论 call 我,我是一个非常乐意帮助你成长的人。有时间我会开个专栏多聊聊自己聊聊生活,欢迎关注我。那么,下面开始正题。
原创不易,看在我每天凌晨两三点刻苦学习和更文的份上,请大家一键三连~
语雀的排版可能会好些:高级前端工程师
// 模拟 forEach
function forEach(array: any[], fn: Function): void {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
forEach([1, 3, 5, 7], (item) => {
console.log(item);
})
// 模拟 filter
function filter(array: any[], fn): any[] {
let result = []
for (let i = 0; i < array.length; i++) {
const item = array[i]
const flag = fn(item)
if (flag) { result.push(item) }
}
return result
}
const filterResult = filter([1, 3, 5, 7], (item) => { return item < 3 })
console.log(filterResult);
③ 函数作为返回值// 函数作为返回值
function add(a: number) {
return function (b: number) { return a + b }
}
const addResult = add(1)(2)
console.log(addResult);
// 模拟用户多次点击但仅执行一次
function once(fn: Function): Function {
let done: boolean = false
return function () {
if (!done) {
done = true
fn.apply(this, arguments);
}
}
}
const onceExample = once((message) => {
console.log(message);
})
onceExample('点击')
onceExample('点击')
// 闭包:调用函数的内部函数,且内部函数访问外部函数作用域中的成员
const mkF = () => {
let number = 1
return (num: number) => {
number = number + num
return number
}
}
const func = mkF()
console.log(func(1));
console.log(func(1));
函数式编程就是我们要可以在项目里不断的抽出一些低阶公共函数,如此我们可以在不同的页面使用到这些公共函数,而且还可以把各种低阶函数组合为高阶函数,通过柯里化等操作把低阶函数揉起来,这样在页面里也有高阶函数可以使用,代码的可维护性和逼格会大大提升。
其它代码:
// 模拟 map
const map = (array: any[], fn: Function) => {
let result = []
forEach(array, (item: any) => {
result.push(fn(item))
})
return result
}
const mapResult = map([1, 3, 5, 7], (item: number) => {
return item + 1
})
console.log(mapResult);
true
的const every = (array: any[], fn: Function) => {
let result = true
forEach(array, (item: any) => {
if (!fn(item)) {
return result = false
}
})
return result
}
const everyResult = every([1, 3, 5, 7], (item: number) => {
return item > 0
})
console.log(everyResult);
false
的const some = (array: any[], fn: Function) => {
let result = false
forEach(array, (item: any) => {
if (fn(item)) {
return result = true
}
})
return result
}
const someResult = some([1, 3, 5, 7], (item: number) => {
return item > 6
})
console.log(someResult);
副作用
。let array = [1,3,5,7,9]
console.log(array.slice(0,3))
console.log(array.slice(0,3))
console.log(array.slice(0,3))
let array = [1,3,5,7,9]
console.log(array.splice(0,3))
console.log(array.splice(0,3))
console.log(array.splice(0,3))
因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来,Lodash
里的memoize
就有缓存纯函数的作用。示例如下:
// 缓存纯函数结果
function getArea(r) {
console.log(r); // 该行在输入参数相同时将仅执行一次
return Math.PI * Math.pow(r, 2)
}
const getAreaMemory = _.memoize(getArea)
console.log(getAreaMemory(4));
console.log(getAreaMemory(4));
console.log(getAreaMemory(4));
那么memoize
的原理是什么呢,我们模拟memoize
的实现代码如下:
// 模拟 memoize
function memoizeMock(fn: Function): Function {
let cache = {}
return function () {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || fn.apply(fn, arguments)
return cache[key]
}
}
const getAreaMemoryMock = memoizeMock(getArea)
console.log(getAreaMemoryMock(4));
console.log(getAreaMemoryMock(4));
console.log(getAreaMemoryMock(4));
根据上面结果,我们能够体会到纯函数的好处:可缓存以提升程序性能。
Lodash
中的FP
模块里的函数都是纯函数,以下暂时简单示例下不是FP
模块的简单使用。
Lodash
的安装安装 `lodash`:`npm install lodash`
Lodash
中 非FP
的函数使用import * as _ from 'lodash'
const array = [1, 3, 5, 7, 9]
// 取数组首位
console.log(_.first(array));
// 取数组末尾
console.log(_.last(array));
// 取数组首位并转字符串
console.log(_.toString(_.first(array)));
// 反转数组
console.log(_.reverse(array));
// 遍历数组
const r = _.each(array, (item) => {
console.log(item);
})
console.log(r);
0.0 有副作用的函数是不纯函数
0.0 不纯函数一定存在副作用
因为纯函数根据相同的输入返回相同的输出,而如果函数依赖有外部的状态,那么只要外部状态发生变化,函数就无法保证相同的输入得到的输出和之前相同
,就会带来副作用,而该外部状态也是副作用的来源。
示例如下:
// 外部变量导致函数不纯
let mini = 18
function checkAge(age) {
return age >= mini
}
此外,副作用的来源还存在于如配置文件
、数据库
、获取用户的输入
等。
所有外部交互都有可能导致副作用,副作用也使得方法通用性下降
、不适合扩展
或可重用性变弱
,同时副作用还会给程序带来安全隐患和不确定性
。
但是副作用不可能完全禁止(如用户名密码肯定还是要放数据库等等),只能尽可能控制它们在可控范围内发生。
/*
* Lodash 中的 FP 模块
* FP 提供了已被柯里化的实用的对函数式编程友好的方法
* FP 提供的方法使用时,参数都是函数优先,数据滞后
* 而 Lodash 中 非FP 模块的方法使用时,则是数据优先,函数滞后
* 数据要放在最后一个参数,这是为了方便柯里化。
*/
import * as _ from 'lodash'
import * as fp from 'lodash/fp'
// 1. FP 的简单使用与 非FP 方法对比
// map + toUpper 示例
const oneResult_1 = fp.map(fp.toUpper, ['a', 'b', 'c']) // FP
const oneResult_2 = _.map(['a', 'b', 'c'], _.toUpper) // 非 FP
// 2. NEVER SAY DIE --> never-say-die
const transferStr = fp.flow(fp.split(' '), fp.map(fp.toLower), fp.join('-'))
const twoResult = transferStr('NEVER SAY DIE')
// 3. lodash 不如 lodash/fp 的地方
// lodash 里的 map 会向指定的处理函数传递3个参数,会导致出现预期之外的结果
// 如下:
const threeResult_1 = _.map(['23', '8', '10'], parseInt)
// 而 FP 里的方法则因已柯里化,所以传入参数不会出现问题
const threeResult_2 = fp.map(parseInt, ['23', '8', '10'])
// lodash 模块
const _ = require('lodash')
_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C']
_.map(['a', 'b', 'c'])
// => ['a', 'b', 'c']
_.split('Hello World', ' ')
// lodash/fp 模块
const fp = require('lodash/fp')
fp.map(fp.toUpper, ['a', 'b', 'c'])
fp.map(fp.toUpper)(['a', 'b', 'c'])
fp.split(' ', 'Hello World')
fp.split(' ')('Hello World')
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE'))
import { LoDashFp } from 'lodash/fp';
const fp: LoDashFp = require('lodash/fp')
/*
* Point Free
* Pointfree 就是如何使用函数式编程的答案
* https://fr.umio.us/favoring-curry/
* http://lucasmreis.github.io/blog/pointfree-javascript/
* http://www.ruanyifeng.com/blog/2017/03/pointfree.html
*/
// 1. point free 概念
// 不使用所要处理的值,只合成运算过程。中文可以译作"无值"风格。
// var addOne = x => x + 1;
// var square = x => x * x;
// 上面是两个简单函数addOne和square。
// 把它们合成一个运算。
// var addOneThenSquare = R.pipe(addOne, square);
// addOneThenSquare(2) // 9
// 上面代码中,addOneThenSquare是一个合成函数。
// 定义它的时候,根本不需要提到要处理的值,这就是 Pointfree。
// Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。
// 上层运算不要直接操作数据,而是通过底层函数去处理。这就要求,将一些常用的操作封装成函数。
// 2.过程式 和 point free 模式 示例
// 非 point free Hello World -> hello_world
function noPointFree(word: string) {
return word.toLowerCase().split(' ').join('_')
}
// point free 模式
const twoPointFree = fp.flow(fp.toLower, fp.split(' '), fp.join('_'))
const twoResult_1 = twoPointFree('Hello World')
const twoResult_2 = twoPointFree('Hello World')
// 3. Point free 复杂案例
// world wild web ==> W. W. W
const threePointFree = fp.flow(fp.upperCase, fp.split(' '), fp.map(fp.first), fp.join('. '))
const threeResult = threePointFree('world wild web')
import { LoDashFp } from 'lodash/fp'
const fp: LoDashFp = require('lodash/fp')
const _ = require('lodash')
/*
* 函子
* 自己的理解:函子就是具有存储原始值、变换原始值、返回新函子功能的容器,表现形式通常以类Class形式抒写。
* 函子具有 map 方法来返回新的函子,还可通过函子控制函数式编程中的副作用
* 需要注意的是,函子传入的函数是纯函数
* http://www.ruanyifeng.com/blog/2017/02/fp-tutorial.html
* 函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。
* 它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。
* 比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。
*/
// 1. 普通函子示例
class Functor {
// 存储原始值/变换原始值
val
constructor(val) {
this.val = val;
}
// 传入新的变换函数,返回新函子
map(f) {
return new Functor(f(this.val));
}
}
const oneResult = new Functor(5).map(val => val + 1)
// 上面代码中,Functor是一个函子,它的map方法接受函数f作为参数,然后返回一个新的函子,里面包含的值是被f处理过的(f(this.val))。
// 一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。
// 上面生成新的函子的时候,用了new命令。这实在太不像函数式编程了,因为new命令是面向对象编程的标志。
// 函数式编程一般约定,函子有一个of方法,用来生成新的容器。
// 2. of方法替换掉new。
class FunctorTwo {
_val
constructor(val) {
this._val = val
}
static of(value) {
return new FunctorTwo(value)
}
map(fn) {
return FunctorTwo.of(fn(this._val))
}
}
const TwoResult = FunctorTwo.of(5).map(val => val + 1).map(val => Math.sqrt(val))
// 3. MayBe 控制副作用,如:null/undefined
// 如果使用链式调用 map 变换过程中值出现了空值情况程序就会崩掉,我们可以使用 MayBe 避免这个问题
class MayBe {
_val
constructor(val) {
this._val = val
}
static of(val) {
return new MayBe(val)
}
map(fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._val))
}
isNothing() {
return this._val === null || this._val === undefined
}
}
const threeResult = MayBe.of(5).map(val => val + 1).map(val => null).map(val => Math.sqrt(val))
console.log(threeResult);
// 4. either 函子:处理异常
// maybe 函子仅能处理空值异常,如果遇到其它异常,则可使用 either 函子来拦截错误信息并正常返回异常函子
// Left 用来拦截错误
class Left {
_val
static of(val) {
return new Left(val)
}
constructor(val) {
this._val = val
}
map(fn) {
return this
}
}
// Right 为正常函子
class Right {
_val
static of(val) {
return new Right(val)
}
constructor(val) {
this._val = val
}
map(fn) {
return Right.of(fn(this._val))
}
}
const jsonParse = (str: string) => {
try {
return Right.of(str).map(val => JSON.parse(val)).map(val => val.name)
} catch (error) {
return Left.of({ error: error.message })
}
}
const fourResult = jsonParse('{name:"zs"}')
// 5. IO 函子
// 若我们希望在函子内使用非纯函数,那么既可以使用IO函子来传入自定义非纯函数
// 函子内仍为纯函数操作,保证非纯操作由使用者自己操作使用
class IO {
_val
static of(val) {
return new IO(function () { return val })
}
constructor(fn) {
this._val = fn
}
map(fn) { // 这里是组合函数而不是调用函数
return new IO(fp.flow(this._val, fn))
}
}
// 此处返回值为Function
const fiveResult = IO.of(process).map(fn => fn.execPath)
console.log(fiveResult._val());
/*
* folktale
* folktale 是一个标准的函数式变成库
* 和 lodash、 ramda不同的是,他没有提供很多功能函数
* 只提供了一些函数式处理的操作,例如: compose、curry等,一些的子Task、 Either、 MayBe等
*/
// 1. folktale 简单使用
import { compose, curry } from 'folktale/core/lambda'
import * as fp from 'lodash/fp'
// - curry
const oneCurry = curry(2, (x, y) => x + y) // arity 指明fn需要几个参数
const oneResult_1 = oneCurry(1)(2)
// - compose
const oneCompose = compose(fp.toUpper, fp.first)
const oneResult_2 = oneCompose(['one', 'two'])
// 2. Task 函子处理异步任务
import { task } from 'folktale/concurrency/task'
import * as fs from 'fs'
import { isIP } from 'net'
function readFile(filename: string) {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
readFile('../package.json').map(fp.split('\n')).map(fp.find((val: string) => val.includes('version')))
.run().listen({
onRejected: err => {
console.log(err);
},
onResolved: value => {
console.log(value);
}
})
// 3. Pointed 函子 - 指实现了 of 方法的函子
// of 避免使用 new 来创建对象,但更深层含义是:把值放到容器中,使用 map 来处理值
// 4. monad 单子 ,单词意思为单细胞
// 我们使用组合函数的方式来解决函数嵌套问题
// 那么函子嵌套问题,IO(IO(x)),我们就会使用 monad 函子来解决
// monad 函子定义:一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad
// 当我们想要合并一个函数,这个函数返回一个值时,我们可以使用monad的map方法
// 当我们想要合并一个函数,这个函数返回一个函子时,我们可以使用monad的flatMap方法
class IO {
_val
static of(val) {
return new IO(function () {
return val
})
}
constructor(fn) {
this._val = fn
}
map(fn) {
return new IO(fp.flow(this._val, fn))
}
join() {
return this._val()
}
flatMap(fn) {
return this.map(fn).join()
}
}
const readFileMonad = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
const printMonad = function (x) {
return new IO(function () {
console.log(x);
return x
})
}
const fourResult = readFileMonad('../package.json').map(fp.upperCase).flatMap(printMonad).join()
// 函数式编程总结
// 1. 认识函数式编程
// 2. 函数相关:函数是一等公民、高阶函数、闭包
// 3. 函数式编程基础:lodash、纯函数、柯里化、管道、函数组合
// 4. 函子
// 1.1 什么是函数式编程?
// 是一种编程范式,和面向对象是一个级别。
// 函数式编程的核心思想是把运算过程抽象成函数。
// 2.1 函数是一等公民的意思?
// 函数也是对象,所以我们可以把函数当作值一样处理,如:把函数作为参数、把函数作为返回值。
// 2.2 高阶函数有哪些?
// 其实就是把函数当作值一样处理,在使用柯里化和函数组合时就是基于高阶函数的。
// 2.3 什么是闭包?
// 3.1 什么是纯函数?
// - 给一个函数相同的输入,总能得到相同的输出,且没有任何的副作用。
// - 纯函数可以理解为就是数学中的函数,通过函数来把输入X和输出Y一一映射。
// - 纯函数的好处有:可缓存、可测试、方便并行处理
// 3.2 柯里化的好处?
// 可以对函数进行降维处理,我们可以把多元函数转化为一元函数,这样做的目的方便我们进行函数组合。
// 3.3 管道和函数组合的联系?
// 我们可以把函数想象为处理数据的管道,我们输入一个数据就会得到一个结果,而函数组合可以把多个一元函数组合成一个功能强大的函数。
// 4.1 什么时函子?
// 函子可以帮助我们控制副作用、进行异常处理或异步操作。
// 函子可以理解为包含一个值和(处理值并返回结果的函数)的容器。
// MayBe 处理空值、Either 处理异常,函子里存储的都是一个值。
// IO 函子存储的是函数,且 of 和别的不一样,可以控制副作用,把不纯的函数处理让使用者传入进去。
// Task 函子用于处理异步任务。
// monad 函子用于处理函子嵌套问题,除了 of 和 map,还有 join 和 flatMap 方法。