函数式编程
函数式编程中的函数,不是指的程序中的函数(方法),而是数学中的映射关系 y=f(x)
函数式编程用来描述数据(函数)之间的映射
函数是一等公民
- 函数可以储存在变量中
- 函数可以作为参数
- 函数可以作为返回值
高阶函数
高阶函数(higher-order-function)
- 可以把函数作为参数传递给另一个函数
- 可以把函数作为另一个函数的返回结果
作用:
- 抽象可以帮我们屏蔽细节,只需要关注我们的目标
- 高阶函数是用来抽象的通用问题
函数作为参数
function forEach (array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
let arr = [1,23,5]
forEach(arr, function (item) {console.log(item)})
function filte (arr, fn) {
var res = []
for (let i = 0; i < array.length; i++) {
const element = array[i];
if(fn(element)) {
res.push(element)
}
}
return res
}
函数作为返回值
//只执行一次的函数
function once (fn) {
let done = false
return function () {
if(!done) {
done = true
return fn.apply(this, arguments)
}
}
}
let pay = once(function(moeny) {
console.log(moeny)
})
pay(5)
闭包Closure
闭包:函数和其周围的状态的引用捆绑在一起,形成的闭包。可以在另一个作用域中,调用函数的内部函数并访问到该函数的作用域成员。
闭包的本质:当函数执行的时候,函数会被防到一个执行栈上去,当函数执行完毕后会从执行栈中移除,但是堆上的作用域成员,因为外部引用不会被释放,所以内部函数依旧可以访问外部函数的成员。
function makeFn () {
let msg = 'hello'
return function () {
console.log(msg)
}
}
let a = makeFn()
a() //此时函数里的msg变量并不会被释放掉
//只执行一次的函数
function once (fn) {
let done = false
return function () {
if(!done) {
done = true
return fn.apply(this, arguments)
}
}
}
let pay = once(function(moeny) {
console.log(moeny)
})
//只会执行一次
pay(5)
pay(5)
应用1: 求次方
//为不同的数求它的几次方
function makePower (number) {
return function (power) {
return Math.pow(number, power)
}
}
//最开始确认到底要求哪个数的次方
let num = makePower(5);
//然后再求这个数的几次方 5的二次方,三次方
num(2);
num(3);
//为不同级别的员工求他们的总工资(基本工资+绩效工资)
function makeSalary (base) {
return function (base, performance) {
return base + performance
}
}
//确定不通风级别的员工
let salary1 = makeSalary(1200)
let salary2 = makeSalary(1300)
//确定此员工的绩效工资
salary1(200)
salary2(300)
纯函数
纯函数:使用相同的输入,永远会得到相同的输出,而且没有可观察的副作用。
lodash: 是一个纯函数的功能库,提供了对数组、字符串、数字、函数等操作的一些方法。
//slice splice
let arr = [1, 2, 3, 5]
//纯函数:相同输入,相同的输出
arr.slice(0, 2)
arr.slice(0, 2)
arr.slice(0, 2)
//不纯函数:相同的输入,不同的输出
arr.splice(0, 1)
arr.splice(0, 1)
arr.splice(0, 1)
纯函数的好处
1.由于纯函数是相同的输入始终都有相同输出,所以可以将输出的结果缓存起来。
2.好测试
3.并行处理
function getArea (r) {
console.log('once')
return Math.PI * r * r
}
//缓存了getArea这个函数,等于只需要从上面拿一次,接下来的调用都从缓存里拿
//模拟memoize方法
function memoize (fn) {
//将函数的参数作为key,将函数返回值作为value
let cache = {}
return function () {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || fn.apply(fn, arguments)
return cache[key]
}
}
const getAreaWithMemoize = memoize(getArea)
getAreaWithMemoize(3) //once
副作用
副作用让一个函数变的不纯,纯函数是相同的输入有着相同的输出结果,如果函数依赖于外部的状态就无法保证相同的输出,就会带来副作用。
副作用的来源
- 全局变量
- 配置文件
- 数据库
- 获取用户输入
- …
所有外部交互都有可能带来副作用,副作用也使得代码通用性下降使得不可扩展和可重用性,同时还有可能给程序带来安全隐患和不确定性,但副作用不可能完全禁止,尽可能控制它们在可控范围内发生。
柯里化
- 当一个函数有多个参数的时候,我们可以先传递一部分参数去调用它(这部分参数以后永远不变)
- 然后返回一个新的函数用来接收剩参数,返回结果
// function checkAge (age) {
// //硬编码
// let min = 18
// return age >= min
// }
function checkAge (min, age) {
return age >= min
}
//以下输出中可能会有经常复用的值,所以可以改进成闭包解决
console.log(checkAge(18, 20))
console.log(checkAge(18, 10))
console.log(checkAge(18, 23))
console.log(checkAge(13, 20))
// function checkAge2 (min) {
// return function (age) {
// return age >= min
// }
// }
let checkAge2 = (min) => ((age) => age >= min)
let min_18 = checkAge2(18)
min_18(24)
min_18(22)
lodash中的柯里化函数
_.curry(func)
- 功能:创建一个函数,该函数接收一个或者多个fnc参数,如果fnc的参数都被提供执行则执行func,并返回执行结果。否则继续返回该函数并等待接收剩余参数。
- 参数:需要柯里化的函数
- 返回值:柯里化后的函数
const _ = require('lodash')
function getSum (a, b, c) {
return a + b + c
}
const curried = _.curry(getSum)
console.log(curried(1, 2, 3)) //6
console.log(curried(1, 2)(4)) //7
console.log(curried(1)(4, 5)) //10
案例:
// ''.match(/\s+/g)
// ''.match(/\d+/g)
/**
*
* @param {*} res 正则表达式
* @param {string} str 字符串
*/
// function match (res, str) {
// return str.match(res)
// }
const match = _.curry(function (res, str) {
return str.match(res)
})
const haveSpace = match(/\s+/g)
console.log(haveSpace('hello world'))
const filter = _.curry(function (func, arr) {
return arr.filter(func)
})
//用来过滤含有空格的字符
console.log(filter(haveSpace, ['h a', 'aa']))
const findSpace = filter(haveSpace)
console.log(findSpace(['h a', 'aa']))
模拟lodash中curry函数
function curry (func) {
return function curriedFn(...args) {
//判断形参跟实参的个数
if(args.length < func.length) {
//实参的个数小于形参的个数
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
function getSum (a, b, c) {
return a + b + c
}
const curried = curry(getSum)
console.log(curried(1, 2, 3)) //6
console.log(curried(1, 2)(4)) //7
console.log(curried(1)(4, 5)) //10
函数组合compose
如果一个函数要通过多个函数(每个函数的参数只能是一个)的处理才能得到最终值,这个时候可以把中间过程函数合并成一个函数。
函数就像是数据的管道,函数组合就是把这些管道接起来,让数据穿过多个管道并形成最终结果。
函数组合默认是从右到左执行
函数组合要满足结合律
//函数组合
function compose (f, g) {
return function (value) {
//函数组合默认从右到左执行
return f(g(value))
}
}
//反转数组
function reverse (arr) {
return arr.reverse()
}
//获取数组中第一个值
function first (arr) {
return arr[0]
}
const last = compose(first, reverse)
console.log(last([1,3,5]))
lodash中的组合函数
lodash中的组合函数flow()
或者flowRight()
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)
console.log(f(['one', 'two']))
模拟lodash中的flowRight
//模拟flowRight
// function componse (...args) {
// return function (value) {
// return args.reverse().reduce(function (acc, fn) {
// return fn(acc)
// }, value)
// }
// }
const componse = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
函数组合调试
//NEVER SAY DIE --> never-say-die
const _ = require('lodash')
//先转换为数组,并且把数组中的数据转为小写,最后用split链接起来
//由于函数组合好每一个函数都只有一个参数,所以将split跟join柯里化
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, arr) => _.join(arr, sep))
//定于debug函数
const v = (value) => {
console.log(value)
return value
}
const map = _.curry((func, arr) => _.map(arr, func))
const f = _.flowRight(join('-'),v, map(_.toLower), split(' '))
console.log(f('NEVER SAY DIE'))
Lodash-fp模块
//lodash-fp
const _ = require('lodash')
//lodash中map方法是数据优先,函数之后
// _.map(['23', '8', '10'], parseInt)
const fp = require('lodash/fp')
//fp方法中map是函数优先
fp.map(parseInt, ['23', '8', '10'])
Point Free
Point Free: 我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合并到一起,在用到这种模式之前我们需要定义一些辅助的基本运算函数。
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
//point free
//Hello World --> hello_world
//转换小写 --> 字符串替换
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
f('Hello World')
//把一个字符串中的首字母提取,并转换为大写,使用.做分隔符
//world wild web --> W.W.W
//用空格切割字符串 --> 数组中的每一个元素转化大写 --> 把数组中每项的第一个字符取出 --> join链接
// const firstLetterToUpper = fp.flowRight(fp.join('.'),fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
//中间两个参数都是map,对数组进行了两次遍历,所以可以进行优化一下
const firstLetterToUpper = fp.flowRight(fp.join('.'),fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
console.log(firstLetterToUpper('world wild web'))
函子(Functor)
- 容器:包含值和值的变形关系(这个变形关系就是函数)
- 函子:是一个特殊的容器,通过一个普通对象来实现,改对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)
//函子
// class Container {
// constructor (value) {
// this._value = value
// }
// map (fn) {
// return new Container(fn(this._value))
// }
// }
// let r = new Container(5)
// .map(x => x + 1)
// .map(x => x + 2)
class Container {
constructor (value) {
this._value = value
}
static of (value) {
return new Container(value)
}
map (fn) {
return Container.of(this._value)
}
}
let r = Container.of(5)
.map(x => x * x)
总结:
- 函数式的编程运算不直接操作值,而是由函子完成
- 函子就是一个实现了map契约的对象
- 可以把函子想象成一个盒子,这个盒子封装了一个值
- 要想处理盒子里的值,我们需要给盒子的map方法传递一个处理值的纯函数,由这个函数来对值进行处理
- 最终map方法返回一个包含新值的盒子
Maybe函子
编程中可能会遇到很多错误,需要对这些错误进行相应的处理,maybe函子的作用就是可以对外部的空值情况做处理(控制副作用在允许范围)
//maybe函子
class MayBe {
constructor (value) {
this._value = value
}
static of (value) {
return new MayBe(value)
}
map (fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
isNothing () {
return this._value === undefined || this._value === null
}
}
// let r = MayBe.of('hello')
// .map(x => x.toUpperCase())
// let r = MayBe.of(undefined)
// .map(x => x.toUpperCase())
// console.log(r)
//无法确定到底是哪次调用出现了问题
let r = MayBe.of('hello')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '))
console.log(r) //MayBe { _value: null }
Either函子
Either两者中任何一个,有点类似于if…else的处理。
异常会让函数变的不纯,Either可以对异常进行处理。
//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))
}
}
// const r1 = Right.of(12).map(x => x + 2)
// const r2 = Left.of(12).map(x => x + 2)
// console.log(r1) //Right { _value: 14 }
// console.log(r2) //Left { _value: 12 }
function pareJSON (str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({error: e.message})
}
}
let r = pareJSON('{ name: zx}')
console.log(r) //Left { _value: { error: 'Unexpected token n in JSON at position 2' } }
let r1 = pareJSON('{ "name": "zx"}')
.map(x => x.name.toUpperCase())
console.log(r1) //Right { _value: 'ZX' }
IO函子
IO:input-output输入输出
IO函子中的_value是一个函数,这里是把函数作为值来处理。
IO函子可以把不纯的动作储存到_value中延迟执行这个不纯的操作(惰性执行),保证当前的操作纯。
把不纯的操作交给调用者来处理。
//IO函子
const fp = require('lodash/fp')
class IO {
constructor (fn) {
this._value = fn
}
static of (value) {
return new IO(function () {
return value
})
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
const r = IO.of(process).map(x => x.execPath)
console.log(r) //IO { _value: [Function] }
console.log(r._value()) ///usr/local/bin/node
folktale
Task异步执行
folktale是一个标准函数式编程库,和lodash、ramda不同的是,他没有提供很多的功能函数。
只提供了一些基本的函数式操作,如compose,curry等,一些函子Task,Either,Maybe等。
const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')
//第一个参数用来指明第二个函数的参数有几个
let f = curry(2, (x, y) => {
return x + y
})
console.log(f(1, 2))
let c = compose(toUpper, first)
console.log(f(['one, two']))
//task处理异步任务
const { task } = require('folktale/concurrency/task')
const fs = require('fs')
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方法里处理返回结果
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {console.log(err)},
onResolved: data=> {console.log(data)} // "version": "1.0.0",
})
Pointed函子
Pointed函子是实现了of方法的函子。
of方法是为了避免new来创建对象,更深层的意思是of方法用来把值放在上下文Context(把值放入容器中,使用map来处理值)。
//IO函子
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
constructor (fn) {
this._value = fn
}
static of (value) {
return new IO(function () {
return value
})
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
// const r = IO.of(process).map(x => x.execPath)
// console.log(r) //IO { _value: [Function] }
// console.log(r._value()) ///usr/local/bin/node
//读取文件操作,由于调用外部资源会有副作用,所以用IO函子处理,这样返回的内容保证是纯的,因为返回的内容是一个IO对象
function readFile (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
function print (x) {
return new IO(function () {
console.log(x)
return x
})
}
let cat = fp.flowRight(print, readFile)
//IO(IO(x))
let r = cat('package.json')._value()._value() //注意这里就很不方便,嵌套几层IO就要写几个_value
console.log(r)
monad函子
monad函子是可以变扁的Pointed函子,IO(IO(X))
如果一个函子拥有join和of两个方法并遵守一些定律就是一个Monad
//IO函子
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
constructor (fn) {
this._value = fn
}
static of (value) {
return new IO(function () {
return value
})
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
join () {
return this._value()
}
flatMap (fn) {
return this.map(fn).join()
}
}
//读取文件操作,由于读取文件操作会有副作用,所以用IO函子处理
function readFile (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
function print (x) {
return new IO(function () {
console.log(x)
return x
})
}
const r = readFile('package.json')
.flatMap(print)
.join()