目录 [toc]
认识函数式编程
函数式编程指北 阮一峰 函数式编程入门
为什么要学习
函数式编程随着 React 的流行受到越来越多的关注
Vue 3 也开始拥抱函数式编程
函数式编程可以抛弃 this
打包过程中可以更好的利用 tree shaking 过滤无用代码
方便测试、并行处理
有很多库可以帮助我们进行函数式开发:lodash、underscore、ramda
函数式编程概述
函数式编程(Function Programming, FP)是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程
面向过程编程:按照 步骤 来实现,一步一步实现想要的功能
面向对象编程:把现实世界中的事物抽象成程序世界中的类和对象,通过 封装、继承和多态 来演示事物之间的联系(对现实世界的事物进行抽象)
函数式编程:把现实世界的事物和事物之间的 联系 抽象到程序世界(对运算过程进行抽象)
函数式编程中的函数指的不是程序中的函数(方法) ,而是数学中的函数(描述映射关系),例如: y = sin(x)
,x 值确定 y 值就确定了
相同的输入始终要得到相同的输出 (纯函数)
函数式编程用来描述数据(函数)之间的映射
1 2 3 4 5 6 7 8 9 10 11 12 let num1 = 2 let num2 = 3 let sum = num1 + num2console .log (sum)function add (n1, n2 ) { return n1 + n2 } let sum = add (2 , 3 )console .log (sum)
一等公民的函数 函数是一等公民
MDN First-class Function(头等函数)
判断一门语言是否支持函数式编程,一个重要的判断标准就是:它是否将函数看做是 “一等公民”
一等公民意味着函数和其它数据类型具备相同地位,可以做如下事情:
函数可以存储在变量中
函数可以作为参数
函数可以作为返回值
在 JavaScript 中 函数就是一个普通的对象 (可以通过 new Function()
创建),我们可以把函数存储到变量或数组中,它还可以作为另一个函数的参数和返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let fn = function ( ) { console .log ('Hello First-class Function' ) } fn ()const BlogController = { index (posts ) { return Views .index (posts) } } const BlogController = { index : Views .index }
高阶函数 高阶函数(Higher-order function)
可以把函数作为参数传递给另一个函数
可以把函数作为另一个函数的返回结果
高阶函数的意义
函数式编程是对运算过程的抽象,抽象可以帮助我们屏蔽细节,只需要关注目标即可
高阶函数是用来抽象通用的问题
函数作为参数: 可以让函数变得更灵活,且在调用时不需要考虑内部是如何实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function forEach (array, fn ) { for (let i = 0 ; i < array.length ; i++) { fn (array[i]) } } let arr = [1 , 3 , 4 , 7 , 8 ]forEach (arr, function (item ) { console .log (item) }) function filter (array, fn ) { let results = [] for (let i = 0 ; i < array.length ; i++) { if (fn (array[i])) { results.push (array[i]) } } return results } filter (arr, function (item ) { return item % 2 === 0 })
函数作为返回值: 让一个函数生成一个函数,这样可以在其中增加一些判断,比如:让函数只执行一次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function makeFn ( ) { let msg = 'Hello function' return function ( ) { console .log (msg) } } makeFn ()()function once (fn ) { let done = false return function ( ) { if (!done) { done = true return fn.apply (this , arguments ) } } } let pay = once (function (money ) { console .log (`支付:${money} RMB` ) }) pay (5 )
常用高阶函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const map = (array, fn ) => { let results = [] for (const value of array) { results.push (fn (value)) } return results } const every = (array, fn ) => { let result = true for (const value of array) { result = fn (value) if (!result) break } return result } const some = (array, fn ) => { let result = false for (let value of array) { result = fn (value) if (result) break } return result } let arr = [1 , 3 , 4 , 9 ]let r = some (arr, v => v % 2 === 0 )console .log (r)
闭包 闭包(Closure):函数和其周围的状态的引用捆绑在一起形成闭包
可以在另一个作用域中调用一个函数的内部函数,并访问到该函数的作用域的成员(延长了外部函数作用的范围)
1 2 3 4 5 6 7 8 function makeFn ( ) { let msg = 'Hello function' return function ( ) { console .log (msg) } } const fn = makeFn ()fn ()
闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除, 但是堆上的作用域成员因为被外部引用不能释放 ,因此内部函数依然可以访问外部函数的成员
调用时观察 Call Stack
(调用栈)和 Scope
(作用域)观察闭包清空
1 2 3 4 5 6 7 8 9 function makePower (power ) { return function (number ) { return Math .pow (number, power) } } let power2 = makePower (2 )let power3 = makePower (3 )console .log (power2 (4 ))console .log (power3 (4 ))
函数式编程基础 纯函数 纯函数:相同的输入永远会得到相同的输出 ,而且没有任何可观察的副作用
纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x)
lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
数组的 slice
和 splice
分别是:纯函数和不纯的函数
slice
返回数组中指定部分,不会改变原数组
splice
对数组进行操作返回改数组,会改变原数组
1 2 3 4 5 6 7 8 9 let array = [1 , 2 , 3 , 4 , 5 ]console .log (array.slice (0 , 3 )) console .log (array.slice (0 , 3 )) console .log (array.splice (0 , 3 )) console .log (array.splice (0 , 3 ))
函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
我们可以把一个函数的执行结果交给另一个函数去处理
lodash 中的纯函数
1 2 3 4 5 6 7 8 9 10 11 const _ = require ('lodash' )const array = ['jack' , 'tom' , 'lucy' , 'kate' ]console .log (_.first (array))console .log (_.last (array))console .log (_.toUpper (_.first (array)))console .log (_.reverse (array))const r = _.each (array, (item, index ) => { console .log (item, index) }) console .log (r)
纯函数好处
可缓存
因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
可测试
纯函数让测试更方便
并行处理
在多线程环境下并行操作共享的内存数据很可能会出现意外情况
纯函数不需要访问共享的内存数据(封闭的空间),所以在并行环境下可以任意运行纯函数(Web Worker)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const _ = require ('lodash' )function getArea (r ) { console .log (r) return Math .PI * r * r } function memoize (f ) { let cache = {} return function ( ) { let key = JSON .stringify (arguments ) cache[key] = cache[key] || f.apply (f, arguments ) console .log (key) return cache[key] } } let getAreaWithMemory = memoize (getArea)console .log (getAreaWithMemory (4 ))console .log (getAreaWithMemory (4 ))
纯函数副作用
纯函数:相同的输入永远会得到相同的输出,而且 没有任何可观察的 副作用
1 2 3 4 5 6 7 8 9 10 11 let min = 18 function checkAge (age ) { return age >= min } function checkAge (age ) { let min = 18 return age >= min }
如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用,副作用来源:
所有的外部交互都有可能产生副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生
柯里化(Haskell Brooks Curry)
当一个函数有多个参数的时候会先传递一部分参数调用它(这部分参数以后永远不变)
然后返回一个新的函数接收剩余的参数,返回结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function checkAge (min, age ) { return age >= min } console .log (checkAge (18 , 20 ))function checkAge (min ) { return function (age ) { return age >= min } } console .log (checkAge (18 )(20 ))let checkAge = min => age => age >= minlet checkAge18 = checkAge (18 )console .log (checkAge18 (24 ))
lodash 中的柯里化函数
_.curry(func)
功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提供则执行 func 并返回执行的结果,否则继续返回该函数并等待接收剩余的参数
参数:需要柯里化的函数
返回值:柯里化后的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const _ = require ('lodash' )'' .match (/\s+/g )const match = _.curry (function (reg, str ) { return str.match (reg) }) const haveSpace = match (/\s+/g )console .log (haveSpace ('hello world' ))const filter = _.curry (function (func, array ) { return array.filter (func) }) const findSpace = filter (haveSpace)console .log (findSpace (['John Connor' , 'John_Donne' ]))
模拟 _.curry()
的实现
getSum
需要几个参数,在调用 curried
时就传递几个参数,立即调用并返回执行结果
调用 curried
时只传递 getSum
需要的部分参数,此时 curried
函数会返回一个新的函数并等待 getSum
所需要的其他参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function getSum (a, b, c ) { return a + b + c } function curry (func ) { return function curriedFn (...args ) { if (args.length < func.length ) { return function ( ) { return curried (...args.concat (Array .from (arguments ))) } } return func (...args) } } const curried = curry (getSum)console .log (curried (1 , 2 , 3 ))console .log (curried (1 , 2 )(3 ))
柯里化总结
柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
柯里化可以帮我们生成一个新的函数,生成的函数已经记住了某些固定的参数
这是一种对函数参数的 “缓存”
让函数变的更灵活,让函数的粒度更小
可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
函数组合 纯函数和柯里化很容易写出洋葱代码:h(g(f(x)))
获取数组的最后一个元素再转换成大写字母:_.toUpper(_.first(_.reverse(array)))
函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
管道
下面这张图表示程序中使用函数处理数据的过程,把参数 a 经过管道 fn 传入,经过处理返回 b
如果管道 fn 特别长(如果哪个地方漏水,可能很难去找到具体位置),可以把管道 fn 拆分成多个。下面这张图表示把 fn 这个管道拆分成 3 个管道 f1、f2、f3,最终把 参数 a 处理后返回 b
1 2 3 fn = compose (f1, f2, f3) b = fn (a)
函数组合: 如果一个函数要经过多个函数才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
函数组合默认是从右到左执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function compose (f, g ) { return function (value ) { return f (g (value)) } } function reverse (array ) { return array.reverse () } function first (array ) { return array[0 ] } const last = compose (first, reverse)console .log (last ([1 , 2 , 3 , 4 ]))
lodash 中的组合函数
lodash 中组合函数 flow()
或 flowRight()
,他们都可以组合多个函数
flow()
是从左到右
flowRight()
是从右到左运行,使用的更多一些
1 2 3 4 5 6 7 const _ = require ('lodash' )const reverse = arr => arr.reverse ()const first = arr => arr[0 ]const toUpper = s => s.toUpperCase ()const f = _.flowRight (toUpper, first, reverse)console .log (f (['one' , 'two' , 'three' ]))
模拟实现 lodash 的 flowRight
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 function compose (...args ) { return function (value ) { return args.reverse ().reduce (function (acc, fn ) { return fn (acc) }, value) } } const compose = (...args ) => value => args.reverse ().reduce ((acc, fn ) => fn (acc), value)
函数的组合要满足 结合律 (associativity)
compose(f, g, h)
我们既可以把 g 和 h 组合 compose(f, compose(g, h))
,还可以把 f 和 g 组合 compose(compose(f, g), h)
,结果都是一样的
1 2 3 4 5 6 const _ = require ('lodash' )const f = _.flowRight (_.toUpper , _.flowRight (_.first , _.reverse ))console .log (f (['one' , 'two' , 'three' ]))
如何调试组合函数
1 2 3 4 5 6 7 8 9 10 11 12 13 const _ = require ('lodash' )const trace = _.curry ((tag, v ) => { console .log (tag, v) return v }) const split = _.curry ((sep, str ) => _.split (str, sep))const join = _.curry ((sep, array ) => _.join (array, sep))const map = _.curry ((fn, array ) => _.map (array, fn))const f = _.flowRight (join ('-' ), trace ('map 之后' ), map (_.toLower ), trace ('split 之后' ), split (' ' ))console .log (f ('NEVER SAY DIE' ))
lodash/fp
lodash 默认调用方式是 _.map(['a', 'b'], _.toUpper)
(数据优先,函数之后)
lodash 的 fp 模块提供了实用的对 函数式编程友好 的方法(函数优先,数据之后)
提供了不可变 auto-curried interatee-first data-last 的方法
1 2 3 4 5 6 7 8 9 const fp = require ('lodash/fp' )fp.map (fp.toUpper , ['a' , 'b' , 'c' ]) fp.split (' ' )('Hello World' ) const f = fp.flowRight (fp.join ('-' ), fp.map (fp.toLower ), fp.split (' ' ))console .log (f ('NEVER SAY DIE' ))
lodash map 的问题
1 2 3 4 5 6 7 8 9 10 11 const _ = require ('lodash' )console .log (_.map (['23' , '8' , '10' ], parseInt ))const fp = require ('lodash/fp' )console .log (fp.map (parseInt , ['23' , '8' , '10' ]))
Point Free
pointfree 指的是永远不说出你的数据,函数无须提及将要操作的数据是什么样的
Point Free: 我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用代表数据的那个参数,只要把简单的运算合并到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数
不需要指明处理的数据
只需要合成运算过程
需要定义一些辅助的基本运算函数
1 2 3 4 5 6 7 8 9 10 function f (word ) { return word.toLowerCase ().replace (/\s+/g , '_' ) } const fp = require ('lodash/fp' )const f = fp.flowRight (fp.replace (/\s+/g , '_' ), fp.toLower )console .log (f ('Hello World' ))
使用 Point Free 模式,把单词中的首字母提取并转换成大写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const fp = require ('lodash/fp' )const firstLetterToUpper = fp.flowRight ( fp.join ('. ' ), fp.map (fp.flowRight (fp.first , fp.toUpper )), fp.split (' ' ) ) console .log (firstLetterToUpper ('world wild web' ))
Functor(函子) 函子其实就可以理解成一个盒子
盒子里面封装了一个值(value),这个值不对外提供,盒子对外提供 map 方法
如果想处理盒子里面的值,就需要给 map 方法里传递一个纯函数来处理这个值
map 返回的是包含新值的盒子,这样就形成了链式调用
如果传递了 null 值或 undefined,就会产生副作用导致函数不纯,这时就可以使用 MayBe 函子
如果 map 方法调用了很多次,返回的是 null,使用 MayBe 函子就比较难进行溯源了,这时就可以使用 Either 函子(类似 if...else...
)
对于不纯的操作也可以使用 IO 函子,把不纯的操作延迟执行
函子可以帮我们控制副作用进行异常处理还可以处理异步任务
Pointed 函子是实现了 of 静态方法的函子
Monad 函子就是静态 IO 方法 + join 方法的函子(当一个函子返回一个函子时就可以使用 Monad)
函子 函数式编程其实就是把运算抽象成函数,将来函数可以做到最大化重用,另外我们还知道函数式编程是建立在数学思想上的,比如:纯函数就是数学中的函数,而函子是建立数学范畴论的基础上(一个范畴到另一个范畴的投影)
什么是 Functor
容器:包含值和值得变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通函数的对象来实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
可以理解为盒子里面有一个值,并且对外公布一个对值处理的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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 (5 ) .map (x => x + 1 ) .map (x => x * x) console .log (r)
函子总结
函数式编程运算不直接操作值,而是由函子完成
函子就是一个实现了 map 契约的对象
我们可以把函子想象成一个盒子,这个盒子里封装了一个值
想要处理盒子中的值,需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
最终 map 方法返回一个包含新值得盒子(函子),所以就可以进行链式调用
在 Functor 中我们传入了 null 或 undefined,这样就会导致函数不纯(副作用),这时需要了解一下 MayBe 函子
1 2 Container .of (null ).map (x => x.toUpperCase ())
MayBe 函子
我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 ('Hello World' ) .map (x => x.toUpperCase ()) .map (x => null ) .map (x => x.split (' ' )) console .log (r)
在多次调用后,最后返回 null 值。这里什么时候出现null,我们是不太容易知道的,就需要了解一下 Either 函子
Either 函子
Either 两者中的任何一个,类似于 if...else...
的处理
异常会让函数变的不纯,Either 函子可以用来做异常处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 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 (e) { return Left .of ({ error : e.message }) } } let r = parseJSON ('{ "name": "zs" }' ).map (x => x.name .toUpperCase ())console .log (r)
IO 函子
IO 函子中的 _value
是一个函数,因为函数式一等公民,这里是把函数作为值来处理
IO 函子可以把不纯的动作存储到 _value
中,延迟执行这个不纯的操作(惰性执行),包装为纯的操作
把不纯的操作交给调用者来处理(类似工作中的甩锅)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const fp = require ('lodash/fp' )class IO { static of (value ) { return new IO (function ( ) { return 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 ())
Task 函子 异步任务使用 folktale 标准的函数式变成库来对 Task 演示
folktale 和 lodash、ramda 不同的是,他没有提供很多功能函数
只提供了一些函数式的操作,例如:compose、curry 等,一些函子 Task、Eitther、MayBe 等
安装
folktale 模块的 compose
和 curry
使用方式
1 2 3 4 5 6 7 8 9 10 11 const { compose, curry } = require ('folktale/core/lambda' )const { first, toUpper } = require ('lodash/fp' )let f = curry (2 , (x, y ) => { return x + y }) console .log (f (1 , 2 ))console .log (f (1 )(2 ))let fun = compose (toUpper, first)console .log (fun (['one' , 'two' ]))
Task 异步执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 ('package.json' ) .map (split ('\n' )) .map (find (x => x.includes ('version' ))) .run () .listen ({ onRejected : err => { console .log (err) }, onResolved : value => { console .log (value) }, }) const version = require ('../package.json' ).version console .log (version)
Pointed 函子
Pointed 函子是实现了 of 静态方法的函子
of 方法是为了避免使用 new 来创建对象(避免看起来很面向对象),更深层的含义是 of 方法用来把值放到上下文 Context(把值放到容器中,使用 map来处理值)
1 2 3 4 5 6 class Container { static of (value ) { return new Container (value) } } Container .of (2 ).map (x => x + 5 )
Monad 函子
图解 Monad
在使用 IO 函数的时候,如果想要同步读取文件,利用 IO 函子会写出如下带嵌套的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const fs = require ('fs' )const fp = require ('lodash/fp' )class IO { static of (value ) { return new IO (function ( ) { return value }) } 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 ( ) { return x }) } let cat = fp.flowRight (print, readFile)let r = cat ('package.json' )._value ()._value ()console .log (r)
这里如果想拿到文件里的信息,就需要好几层 ._value()
的调用,这时就可以了解一下 Monad 函子
Monad 函子是可以变扁的 Pointed 函子,IO(IO(X))
一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const fs = require ('fs' )const fp = require ('lodash/fp' )class IO { static of (value ) { return new IO (function ( ) { return 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 = function (filename ) { return new IO (function ( ) { return fs.readFileSync (filename, 'utf-8' ) }) } let print = function (x ) { return new IO (function ( ) { return x }) } let r = readFile ('package.json' ).map (fp.toUpper ).flatMap (print).join ()console .log (r)