函数式编程(FP)
函数式编程
- 函数式编程是编程范式之一,对运算过程进行抽象。
- 函数式编程可以增加函数的重用率
- 相同的输入要有相同的输出
- 函数式编程的函数是数学中的函数。
- 函数式一等公民
- 函数可以存储在变量中
let fn = function(){ console.log('hello owrld') }
- 函数作为参数
- 函数作为返回值
高阶函数
- 定义:
- 可以把函数作为参数传递给另一个函数
优点:可以让函数更灵活//示例1 // function forEach(array,fn){ // for (let i = 1;i<array.length;i++){ // fn(array[i]) // } // } // let arr = [1,3,5.,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 } let arr = [1,3,5,6,7,8] let r = filter(arr,function (item){ return item % 2 === 0 }) console.log(r)
- 可以把函数作为另一个函数的返回结果
//示例1 function makeFn(){ let msg = 'Hello World' return function(){ console.log(msg) } } const fn = makeFn() fn() //示例二 once函数 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) pay(5) pay(5) pay(5) pay(5)
- 高阶函数的意义
- 使函数变得更灵活
- 抽象通用的问题,屏蔽细节,只需要关注目标
- 常用的高阶函数
- 模拟常用的高阶函数
// map const map = (array,fn)=>{ let results = [] for(let value of array){ results.push(fn(value)) } return results } //测试map let arr = [1,2,3,4,5] arr = map(arr,v=>v*v) console.log(arr) // every const every = (array,fn) =>{ let result = true for(let value of array ){ result = fn(value) if(!result){ break } } return result } //测试every let arr[11,12,13,14,15] let r = every(arr,v =>v>10) console.log(r) // some const some = (array,fn)=>{ let result = false for(let value of array){ result = fn(values) if(result){ break } } return result } //测试some let arr = [1,2,3,4,5] let r = some(arr,v => v % 2 === 0) console.log(r)
闭包
- 闭包的概念
- 函数和起周围的状态(词法环境)的引用捆绑在一起形成闭包
- 在一个作用域中调用一个函数内部函数并访问到该函数的作用域中的成员
- 闭包的本质
- 函数在执行的时候回放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数成员。
- 优点,扩大外部函数成员的作用范围
- 闭包的案例
//案例1 function makePower(power){ return function(number){ return Math.pow(number,power) } } let power2 = makePower(2) let power3 = makePower(3) console.log(power2(3)) //案例2 //getSalary(12000,2000) //getSalary(15000,3000) //getSalary(15000,4000) function makeSalary(base){ return function(performance){ return base + performance } } let salaryLevel1 = makeSalary(12000) let salaryLevel2 = makeSalary(15000) console.log(salaryLevel1(2000)) console.log(salaryLevel2(3000))
纯函数
- 概念
- 相同的输入只能得到相同的输出,而且没有可观察到的副作用的函数
- 纯函数就类似数学中的函数,用来描述输入和输出间的映射关系(lodash就是一个纯函数的库)。
- 纯函数的代码演示
//演示纯函数和不纯函数 let array = [1,2,3,4,5] //输出相同,说明slice是纯函数 console.log(array.slice(0,3)) console.log(array.slice(0,3)) console.log(array.slice(0,3)) //输出不相同,说明splice是不纯的函数 console.log(array.splice(0,3)) console.log(array.splice(0,3)) console.log(array.splice(0,3)) //自己写一个纯函数 function getSum(n1,n2){ return n1 + n2 } console.log(getSum(1,2)) console.log(getSum(1,2)) console.log(getSum(1,2))
- 函数式编程不会保留计算的中间结果,所以函数式编程中变量是不可变的(无状态的)
- Lodash 纯函数的库
- 安装和演示lodash:
//安装lodash
npm init -y //初始化package.js
npm i lodash
// 演示lodash const _ = require('lodash')//引用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)
- 安装和演示lodash:
- 纯函数的优势
- 可缓存:因为纯函数对相同的输入有相同的结果,所以可以把结果缓存下来提高效率
//记忆函数 const _ = require('lodash') function getArea(r){ console.log(r) return Math.PI * r *r } let GetAreaWithMemory = _.memoize(getArea) console.log(GetAreaWithMemory(4)) console.log(GetAreaWithMemory(4)) console.log(GetAreaWithMemory(4)) //模拟memoize方法的实现 function memoize (f) { let cache = {} return function(){ let key = JOSN.striongify(arguments) cache[ket] = cache[key] || f.apply(f,arguments) return cache[key] } } let GetAreaWithMemory = memoize(getArea) console.log(GetAreaWithMemory(4)) console.log(GetAreaWithMemory(4)) console.log(GetAreaWithMemory(4))
- 可测试:
- 方便并行处理
- 纯函数的副作用
- 副作用让一个函数变得不纯:纯函数根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保障输出相同,就会带来副作用。
- 副作用的来源
1、配置文件
2、数据库
3、获取用户的输入
所有的外部交互都有可能带来副作用,副作用也石凳方法通用性下降,不利于程序的扩展和可重用性,同时副作用会给程序带来安全隐患和不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。
- 柯里化
- 使用柯里化解决硬编码的问题
//柯里化演示 function chechAge(age){ let min = 18 return age >=min } //普通的纯函数 function chechAge(min,age){ return age >=min } console.log(checkAge(18,20)) console.log(checkAge(18,24)) console.log(checkAge(18,25)) //函数的柯里化 function checkAge(min){ return function(age){ return age >= min } } //另一种写法 let checkAge = min =>(age => age >= min) let checkAge18 = checkAge(18) let checkAge20 = checkAge(20) console.log(checkAge18(20)) console.log(checkAge20(24))
- 柯里化的概念:当一个函数有多个参数的时候先传一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接受剩余的参数,返回结果。
- lodash 中提供的通用的柯里化
_.curry(func)
功能:创建一个函数,该函数接受一个或多个func的参数,如果fenc所需要的参数都被提供则执行func并返回执行结果,否则继续返回该函数并等待接受剩余的参数。
//lodash 中的 curry 基本使用 const _ = require('lodash') function getSum(a,b,c){ return a + b + c } const curried = _.curry(getSum) console.log(curried(1,2,3)) console.log(curried(1)(2,3)) console.log(curried(1,2)(3))
- 柯里化的案例
// 柯里化案例 const _ = require('lodash') function match (reg , str) { return str.match(reg) } const match = _.curry(function (reg , str) { return str.match(reg) }) const haveSpace = match(/\s+/g) const haveNumber = match(/\d+/g) const filter = _.curry(function (func,array){ return array.filter(func) }) const filter2 = _.curry((func,array) => array.filter(func)) const findSpace = filter(haveSpace) console.log(haveSpace('hello world')) console.log(haveNumber('1234abc') console.log(filter(haveSpace,['Johnm Connor','John_Donne'])) console.log(findSpace(['Johnm Connor','John_Donne']))
- 柯里化的实现原理
//模拟实现 lodash中的curry方法 const _ = require('lodash') function getSum(a,b,c){ return a + b + c } const curried = _.curry(getSum) console.log(curried(1,2,3)) console.log(curried(1)(2,3)) console.log(curried(1,2)(3)) function curry (func){ return function curriedFn (...args){ //判断实参和形参的个数 if(args.length < func.length){ return function (){ return curriedFn(...args.concat(Array.from(arguments))) } } return func(...args) } }
- 总结柯里化
1、柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
2、这是一种对函数参数的‘缓存’
3、让函数变得更灵活了,让函数的粒度更小
4、可以把多元函数转换成一元函数,可以组合成功能更强大的函数
函数的组合
纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))
数据管道
-
函数组合
- 概念:如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程函数合并成一个函数
· 函数就像是数据的管道,函数组合就是把这些管道结合起来,让数据穿过多个管道形成最终结果、
· 函数的组合默认是从右到左执行
// 函数组合演示 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中的组合函数
· flow()从左往右执行,flowRight()从右往左执行
// lodash中的函数组合方法 _.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','three']))
- 组合函数中原理模拟
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) } } const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc),value) const f = compose(toUpper,first,reverse) console.log(f(['one','two','three']))
- 函数组合要满足结合律
就是a+b+c = a+(b+c) = a+c+b
//函数组合要满足结合律 const _ = require('lodash') const f = _.flowRight(_.toUpper,_.first,_.reverse) const f = _.flowRight(_.toUpper,_.first,_.reverse) const f = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse) console.log(f(['one','two','three']))
- 函数组合如何调试
//函数组合 调试 // NEVER SAY DIE --> never-say-die const _ = require('lodash') const split = _.curry((sepmstr) => _.split(str,spe)) _.toLower() const join = _.curry((sep,array) => _.join(array,sep)) const f = _.flowRight(join('-'),_.toLower,split(' ')) console.log('NEVER SAY DIE')
- Lodash-FP模块提供了很多已经柯里化之后的方法,如果有多个参数,都是函数优先,数据之后 //传入参数时先传入函数,在传入数据
//lodash的FP 模块 const fp = require('lodash/fp') const f = fp/flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' ')) console.log(f('Never Say Die')) //lodash 和 lodash/fp中map方法的区别 //lodash map const _ = require('lodash') console.log(_.map(['23','8','10'],parseInt)) // parseInt('23',0,array) // parseInt('8',1,array) // parseInt('10',2,array) //lodash/fp中的map const fp = require('lodash/fp') console.log(fp.map(['23','8','10']))
- Point Free
概念:一种代码规范,不需要指明处理的数据,只需要合成运算过程,需要定义一些辅助的基本运算函数
// Point Free // Hello World => hello_world const fp = require('lodash/fp') const f = fp.flowRight(fp.replace(/\s+/g,'_'),fp.toLower) console.log(f(' Hello World'))
// Point Free 案例 // 把一个字符串中的首字母提取出来并转换成大写,使用.作为分隔符 // world wold web ==> w.w.w const fp = require('lodash/fp') //const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' ')) const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first,fp.toUpper)) fp.split(' ')) console.log(firstLetterToUpper('world wold web '))
- 概念:如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程函数合并成一个函数
函子
- 容器:包含值和值的变形关系(函数)
- 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)
// Functor 函子 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 * x) console.log(r) 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方法返回一个包含新值的盒子(函子)
- MayBe函子
// MayBe 函子 :解决value是null或undefined的问题 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) // 无法获取在map 的执行过程中那个步骤出现了null
- Either 函子
- either :两者中的任何一个,类似于if…else…的处理
- 异常会让函数变得不纯,Either函子可以用来做异常处理
// Either 函子 class Left { static of (value) { return new Left(value) } constructor(value){ this._value = value } map (fn){ return this } } class Right { static of (value) { return new Right(value) } constructor(value){ this._value = value } map (fn){ return Right.of(fn(this._value)) } } let r1 = Right.of(12).map(x => x + 2) let r2 = left.of(12).map(x => x + 2) console.log(r1) console.log(r2) function parseJOSN (str){ try{ return Right.of(JOSN.parse(str)) }catch(e){ return Left.of({error: e.message}) } } let r = parseJOSN('{ name: zs }') console.log(r) let r = parseJOSN('{ "name": "zs" }') .map(x => x.name.toUpperCase()) console.log(r)
- IO函子
- IO函子中的_value是一个函数,这里是把函数作为值来处理
- IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的纯操作
- 把不纯的操作交给调用者来处理
//IO函子 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) console.log(r._value())
- folktale 函数式编程库
- 该库只提供了一些函数式处理的操作,例如:compose/curry等,一些函子Task/Either/MayBe等
const { compose, curry } = require('folktale/core/lambda') const { toUpper, first } = require('lodash/fp') let f = cyrry(2,(x, y) => { return x + y }) console.log(f(1, 2)) console.log(f(1)(2)) let f= compose(toUpper,first) console.log(f(['one', 'tow']))
- Task函子处理异步任务
//Task 处理异步任务 const {split, find } = require('lodash/fp') const fs = require('fs') const {task } = require('folktale/concurrency/task') 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) } })
- Pointed 函子:实现了of方法的函子
of方法的作用:1、避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中使用map来处理)
- Monad函子:可以变扁的Pointed函子,如果一个函数具有join和of两个方法并遵守一些定律,就是一个Monad