1. 函数式编程
1.1. 函数是一等公民
函数式编程(Functional Programming, FP)
- 面向对象编程: 把现实世界中的事物抽象成程序世界中的类和对象,通过封装,集成,多态来演示事物事件的联系
- 函数式编程: 把现实世界的事物和事物之间的联系抽象到程序世界,对运算过程进行抽象
- 思维方式 函数式编程中的函数不是指程序中的函数,更多的是数学中的函数即映射关系 相同的输入始终得到相同的输出(纯函数) 函数式编程用来描述函数之间的映射
- 好处: 让函数,逻辑等得到最大程度的重用
- 坏处: 大量使用闭包,某种程度上会降低性能
一等公民体现在函数可以作为变量,可以作为参数,可以作为返回值
- 作为变量时,应用场景如mvc中签名相同的函数
使用高阶函数意义
- 屏蔽细节,只需关注我们的目标
- 高阶函数是用来抽象通用的问题
1.2. 纯函数
如数组的slice和splice方法分别为纯函数和非纯函数
相同的输入得到相同的输出,没有副作用
纯函数好处
- 可以把纯函数的结果缓存起来,节省计算性能
function memoize(fn) { let cache = {} return function() { let key = JSON.stringify(arguments) cache[key] = cache[key] || fn.apply(f, arguments) return cache[key] } }
- 让测试更方便
- 并行环境下可以方便并行处理(web worker)
副作用
副作用来源,依赖外部状态
- 配置文件
- 数据库
- 获取用户输入
副作用不可避免,只能最大程度控制在可控范围内
1.3. 柯里化
函数有多个参数时,调用一个函数传递部分参数,返回一个新的函数,新函数等待接收剩余参数,返回最终结果
tips: 注意传参的顺序性
let checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18)
console.log(checkAge18(20))
1.3.1. lodash中的柯里化函数
_.curry(func)
作用: 将多元函数变成单元函数,方便组合使用
function getSum (a, b, c) {
return a + b + c
}
const curried = _.curry(getSum)
console.log(curried(1, 2, 3), curried(1)(2, 3))
// 去除数组空白项
const match = _.curry(function (reg, str) {
return str.match(reg)
})
const haveSpace = match(/\s+/g)
let filter = _.curry((func/* 筛选数组项的函数 */, arr) => arr.filter(func))
const findSpace = filter(haveSpace)
实现原理
function curry (func) {
return function curriedFn (...args) {
// 判断实参args和形参个数
if(args.length < func.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
总结
- 使用闭包缓存函数参数
- 让函数变得更灵活,粒度更小
- 可以把多元函数转换成一元函数,组合函数产生强大的功能
1.4. 函数组合
纯函数和柯里化容易写出洋葱代码,通过函数组合可以把细粒度的函数重新组合生成一个新的函数
关于函数组合(compose)
- 函数就像是数据的管道,函数组合就是把管道连接起来,让数据穿过多个管道
- 函数组合默认从右到左执行
1.4.1. lodash中的函数组合
flow
,flowRight
const _ = require('lodash')
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const f = _.flowRight(toUpper, first, reverse)
实现原理
// 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)
函数组合要满足结合律
// 如得到一个数组最后一项的大写
_.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
1.4.2. compose调试
在函数前后加上log函数,如
// 缺点无法准确定位数据来自哪个管道
const log = v => {
console.log(v);
return v
}
// 添加标记,使用柯里化后的log函数标记
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
在使用函数组合的适合,如果使用lodash中的函数,如果函数有多个函数,需要重新包装函数对其柯里化,比较麻烦
1.5. lodash中的fp模块
lodash/fp模块提供了对函数式编程友好的方法,及不可变的方法(auto curried iteratee-first data-last)
const _ = require('lodash')
const fp = require('lodash/fp')
_.map(['a', 'b', 'c'], _.toUpper)
fp.map(fp.toUpper, ['a', 'b', 'c']) // 或
fp.map(fp.toUpper)(['a', 'b', 'c'])
案例
// NEVER SAY DIE --> never-say-die
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower), fp.split(' '))
lodash中map方法的问题
_.map
第二个参数的函数需要接收三个参数, value, index|key, collection,当调用parseInt时,会有预料之外的结果,如
_.map(['23', '8', '10'], parseInt) // 结果为[23, NaN, 2],原因是parseInt第二参数为进制
fp模块中的map方法中的函数参数只有一个参数, 无此问题
1.6. 函子(Functor)
- 容器: 包含值和值的变形关系
- 函子: 是一个特殊容器,通过一个普通对象实现,该对象具有map方法,map方法可以运行一个函数对值进行处理.
作用: 控制副作用,处理异常,进行异步操作
class Container {
constructor (value) {
// 函子里要维护一个值,这个值不对外公布.想要处理值,通过map方法,传递处理值的函数
this._value = value
}
map (fn) {
return new Container(fn(this._value))
}
}
new Container(5)
.map(x => x + 1)
.map(x => x * x)
以上写法为面向对象的思想,改造为函数式编程的思想如下
class Container {
constructor (value) {
this._value = value
}
static of (value) {
return new Container(value)
}
map (fn) {
return Container.of(fn(this._value))
}
}
总结
- 函数式编程运算不直接操作值,而是由函子完成
- 函子就是一个实现了map契约的对象
- 可以把函子想象成一个盒子,这个盒子封装了一个值
- 处理盒子中的值,需要给盒子的map方法传递纯函数
- map方法返回一个包含新值的盒子(函子)
1.6.1. MayBe函子
编程过程中出现的错误,需要做出相应处理.MayBe函子可以对外部的空值情况做处理(控制副作用在允许的范围)
class MayBe {
constructor (value) {
this._value = value
}
static of (value) {
return new MayBe(fn(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)
1.6.2. Either函子
- 能给出有效提示信息
- 类似于if...else...的处理
- 异常会让函数变得不纯,Either函子可以用来做异常处理
class Left {
constructor (value) {
this._value = value
}
static of (value) {
return new Left(value)
}
map (fn) {
return this
}
}
class Right {
constructor (value) {
this._value = value
}
static of (value) {
return new Right(value)
}
map (fn) {
return Right.of(fn(this._value))
}
}
let r1 = Right.of(12).map(x => x + 2) // 14
let r2 = Left.of(12).map(x => x + 2) // 12
function parseJSON (str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({ error: e.message })
}
}
1.6.3. IO函子
- IO函子中的_value是一个函数,即把函数当做值来处理
- IO函子可以把不纯的行为存储到_value中,延迟执行不纯的操作,包装的操作为纯的
- 把不纯的操作交给调用者处理
class IO {
constructor (fn) {
this._value = fn
}
static of (value) {
return new IO(function () {
return value
})
}
map (fn) {
// 把第一次调用of传入的函数包装后的函数同传入的函数组合
return new IO(fp.flowRight(fn, this._value))
}
}
// 调用
let r = IO.of(process).map(p => p.execPath)
console.log(r._value())
1.6.4. folktale
一个标准的函数式编程库,与lodash, ramda不同的是,没有提供很多功能函数,只提供了一些函数式处理的操作如函数组合,柯里化等,一些函子
const { compose, curry } = require('folktale/core/lamda')
// curry接收两个参数,参数一为参数二函数中的参数个数
1.6.5. Task函子
进行异步处理
读取packge.json version
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')
function readFile (filename) {
return task(resolver => { // task函数返回一个Task类型的函子
fs.readFile(filename, 'utf-8', (err, data) => {
if(err) resolver.reject(err)
resolver.resolve(data)
})
})
}
readFile('package.json')
.map(spit('\n'))
.map(found(x => x.inclueds('version')))
.run()
.listen({
onRejected: err => {
},
onResolved: value => {
}
})
1.6.6. Pointed函子
- 实现了of静态方法的函子
- 为了避免使用new来创建对象,更深的含义是of方法用来把值放到上下文,即函子中
1.6.7. Monad函子
上面的IO函子的写法,会带来IO函子嵌套的问题,因此Monad可以解决此问题,它是一个实现了join和静态of方法的函子,用于拍平Pointed函子
实现linux/unix下cat命令效果
class Monad {
constructor (fn) {
this._value = fn
}
static of (value) {
return new Monad(function () {
return value
})
}
map (fn) {
return new Monad(fp.flowRight(fn, this._value))
}
join () {
return this._value()
}
flatMap (fn) {
return this.map(fn).join()
}
}
let readFile = function (filename) {
return new Monad(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
let print = function (x) {
return new Monad(function () {
console.log(x)
return x
})
}
// 调用map还是flatMap?当我们要合并的这个函数返回的是值,调用map,返回的是函子,返回flatMap
let r = readFile('package.json')
// 可以在这里使用map对读取后的值进行操作
.flatMap(print)
.join()
console.log(r)