函数式编程
函数式编程(Functional Programming,FP),FP是编程范式之一。常见的编程范式还有面向过程编程、面向对象编程。
-
面向对象编程思维方式:
把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的练习
例如: Class Car{ constructor(){ this.wheel=4; } star(){ //启动 } }
-
函数式编程的思维方式:
把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
程序的本质:输入通过某种运算获得相应的输出,程序开发过程中会涉及到很多输入和输出的函数
函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如y=sin(x),x和y的关系
相同的输入始终要得到相同的输出(纯函数)
函数式编程用来描述数据(函数)之间的映射
//非函数式 let num1 = 2 let num2 = 3 let sum = num1 +num2 console.log(sum) //函数式 对运算过程的抽象 function add(n1,n2){ return n1+n2 } let sum = add(2,3) console.log(sum) 优点:可复用
函数是一等公民
MDN First-class Function
-
函数可以存储在变量中
-
函数作为参数
-
函数作为返回值
在JavaScript中函数就是一个普通的对象(可以通过new Function()),我们可以把函数存储到变量/数组中,它还可以作为另一个函数的参数和返回值,甚至可以在程序运行的时候通过new Function(‘alert(1)’)来构造一个新的函数。
高阶函数
高阶函数(Higher-order function)
-
可以把函数作为参数传递给另一个函数
function forEach(arr,fn){ for (var i =0;i<arr.length;i++){ fn(arr[i]) } } forEach([1,2,3],(item)=>{ console.log(item) })
-
可以把函数作为另一个函数的返回结果
//只执行一次 function once(fn){ let flag=false; return function() { if(!flag){ flag=true; return fn.apply(this,arguments) } } } let pay = once(function(money){ console.log(money) }) pay(5) √ false=>true pay(5) × true pay(5) × true
使用高阶函数的意义
- 抽象可以帮我们屏蔽细节,只需要关注于目标
- 高阶函数是用来抽象通用的问题
闭包
闭包(Closure):
- 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包
- 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
闭包的本质:
函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员
闭包的优点:
延长了函数内部变量的作用范围
纯函数
相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
-
纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y=f(x)
-
lodash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
-
数组的slice和splice分别是:纯函数和不纯函数
slice返回数组中的指定部分,不会改变数组
let arr=[1,2,3,4,5,6] //slice 纯函数,相同输入得到相同输出 console.log(arr.slice(0,3)) //[1,2,3] console.log(arr.slice(0,3)) //[1,2,3] console.log(arr.slice(0,3)) //[1,2,3]
splice对数组进行操作返回该数组,会改变原数组
let arr=[1,2,3,4,5,6] //splice 不纯函数,相同输入得到不同输出 console.log(arr.splice(0,3)) //[1,2,3] console.log(arr.splice(0,3)) //[4,5,6] console.log(arr.splice(0,3)) //[]
-
函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
-
我们可以把一个函数的执行结果交给另一个函数去处理
纯函数的优点:
-
可缓存
因为纯函数对于相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
//模拟一个memoize函数 //求面积 function getArea(r){ console.log('run') return Math.PI*r*r } function memoize(fn){ let cache={} return function(){ const params=JSON.stringify(arguments) cache[params]=cache[params]?cache[params]:fn.apply(this,arguments) return cache[params] } } let getAreaWithMemory=memoize(getArea) console.log(getAreaWithMemory(4)) //run 50.26548245743669 console.log(getAreaWithMemory(4)) // 50.26548245743669 console.log(getAreaWithMemory(4)) // 50.26548245743669
-
可测试
纯函数让测试更方便
-
并行处理
在多线程环境下,并行操作共享的内存数据很可能会出现意外情况
纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker可以开启多线程)
副作用
纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
//不纯 全局变量mini的值发生变化后,相同输入可能返回不同结果
let mini=18
function checkAge(age){
return age>=mini
}
//纯函数
function checkAge(age){
let mini=18
return age>=mini
}
副作用让一个函数变得不纯(如上例),纯函数根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
副作用来源:
- 配置文件
- 数据库
- 获取用户输入
- …
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降,不适合扩展和可重用性,同时副作用会给程序带来安全隐患,给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。
柯里化
柯里化(Haskell Brooks Curry):当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接受剩余的参数,返回结果
柯里化原理模拟:
// 模拟实现loadsh中的curry方法
const _ = require('lodash')
function getSum(a, b, c) {
return a + b + c;
}
const curryGetSum = _.curry(getSum)
console.log(curryGetSum(1)(2)(3));
console.log(curryGetSum(1, 2)(3));
console.log(curryGetSum(1, 2, 3));
function myCurry(func) {
return function curryFnc(...args) {
// 当前已获取参数个数小于所需参数个数,返回接收剩余参数等待调用的函数
if (args.length < func.length) {
return function(){
// 通过闭包引用外层接收到的参数args,拼接本次接收到的参数arguments,再将参数传给curryFnc继续判断参数个数是否满足
return curryFnc(...args.concat(Array.from(arguments)))
}
}
// 参数个数满足要求,调用函数
return func.apply(this, args)
}
}
const c1 = myCurry((a, b, c, d, e) => {
console.log('我被调用了', a + b + c + d + e);
})
c1(1)(2)(3)(4)(5)
总结:
- 柯里化可以让我们给一个函数传递较少的参数,得到一个已经记住了某些固定参数的新函数
- 这是一种对函数参数的’缓存’
- 让函数变得更灵活,让函数的粒度更小
- 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
函数组合
纯函数和柯里化很容易写出洋葱代码h(g(f(x)))
//获取数组最后一个元素,并转换为大写字母:
_.toUpper(_.first(_.reverse(array)))
函数组合(compose):
如果一个函数需要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
- 函数组合可以让我们把细粒度的函数,重新组合生成一个新的函数。
- 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终的结果
- 函数组合默认是从右到左执行的
lodash中的函数组合:
// lodash中的函数组合
const _ = require('lodash')
// 求数组中最后一个元素并转换为大写
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
// _.flowRight组合函数
const f = _.flowRight(toUpper, first, reverse)
console.log(f(['abc', 'bbc', 'cbc']));
模拟lodash中的flowRight:
// 模拟lodash中的flowRight
// 求数组中最后一个元素并转换为大写
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(preRes,curFunc){
return curFunc(preRes)
},value)
}
}
const f=compose(toUpper,first,reverse)
console.log(f(['one','two','three']));
// ES6
const compose1=(...args)=>value=>args.reverse().reduce((preRes,curFunc)=>curFunc(preRes),value)
const f1=compose1(toUpper,first,reverse)
console.log(f1(['oneParams','twoParams','threeParams']));
函数组合调试:
// 函数组合 调试
// NEVER SAY DIE ==> never-say-die
const _ = require('lodash')
let res = _.toLower(_.split('one two three', ' '))
let str='NEVER SAY DIE'
const log=v=>{
console.log(v);
return v;
}
const trace=_.curry((name,v)=>{
console.log(name,v);
return v
})
const split = _.curry((spe, str) => _.split(str, spe))
const join = _.curry((spe, arr) => _.join(arr, spe))
const map=_.curry((fn,arr)=>_.map(arr,fn))
const f=_.flowRight(join('-'),log,_.toLower,split(' ')) //错误
const f1=_.flowRight(join('-'),map(_.toLower),trace('split'),split(' ')) //正确
console.log(f(str));
console.log(f1(str));
lodash/fp:
- lodash的fp模块提供了实用的对函数式编程友好的方法
- 提供了不可变auto-curried iteratee-first data-last的方法(自动柯里化,函数优先,数据滞后)
// lodash中的 fp 模块
// NEVER SAY DIE ==> never-say-die
const fp = require('lodash/fp')
let str='NEVER SAY DIE'
const f=fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '))
console.log(f(str));
PointFree
PointFree:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
- 不需要指明处理的数据
- 需要合成运算过程
- 需要定义一些辅助的基本运算函数
const f = fp.flowRight(fp.join('-'),fp.map(_.toLower),fp.split(' '))
// 非 Point Free 模式 // Hello World => hello_world
function f(word) {
return word.toLowerCase().replace(/\s+/g, '_');
}
// Point Free
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World'))
函子
为什么要学函子
到目前为止已经学习了函数式编程的一些基础,但我们还有没演示在函数式编程中如何把副作用控制在可控范围内,如异常处理、异步操作等。
什么是Functor
容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)
// Functor 函子 一个容器,具有map方法,map方法可以运行一个函数对值进行处理(形变关系)
class Container {
constructor(value) {
this._value = value
}
map(fn) {
return new Container(fn(this._value))
}
}
let res = new Container(5).map(x => x + 1).map(x => x * x)
console.log('res', res);
// 优化new Container
class Container1 {
// 定义静态方法,封装new的过程,通过类名调用of生成Container实例
static of (value) {
return new Container(value)
}
constructor(value) {
this._value = value
}
map(fn) {
return Container1.of(fn(this._value))
}
}
let res1 = Container1.of(5).map(x => x + 1).map(x => x * x)
console.log('res1', res1);
总结
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了map契约的对象
- 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
- 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
- 最终map方法返回一个包含新值的盒子(函子)
MayBe函子
-
我们在编程的过程中可能会遇到很多错误,需要对这些错误进行相应的处理
-
MayBe函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
// MayBe函子 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 res1 = MayBe.of('Hello World').map(item => item.toUpperCase()) console.log(res1); let res2 = MayBe.of(null) .map(item => item.toUpperCase()) console.log(res2); // MayBe函子处理空值时,无法知道出现空值的位置 let res3 = MayBe.of('hello world') .map(x => x.toUpperCase()) .map(x => null) .map(x => x.split(' ')) console.log(res3);
Either函子
- Either 两者中的任何一个,类似于if…else…的处理
- 异常会让函数变得不纯,Either函子可以用来做异常处理
// Either函子 Either 两者中的任何一个,类似于if...else...的处理
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 = Left.of(12).map(x => x + 2)
// let r2 = Right.of(12).map(x => x + 2)
// console.log(r1);
// console.log(r2);
function parseJSON(str) {
try {
return Right.of(str).map(item => JSON.parse(item))
} catch (e) {
return Left.of({
error: e.message
})
}
}
// 错误数据
console.log(parseJSON("{name:xiaoming}"));
// 正确数据
let r=parseJSON('{"name":"xiaoming"}').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))
}
}
const r=IO.of(process).map(p=>p.execPath)
// 调用 IO.of(process) 生成了一个用 function(){return process} 初始化的IO函子
// 随后调用该函子的map方法,将 p=>p.execPath 方法传给map形参fn
// map中再生成一个新的函子,将先前初始化后的值(function(){return process})和当前传入的fn(p=>p.execPath)组合后作为新函子的初始值传入,再返回该函子。
console.log(r);
console.log(r._value());
Task函子
使用folktale中的Task函子处理异步任务
// Task 处理异步任务
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('z-myfile.json')
.map(split('\n'))
.map(find(item=>item.includes('time')))
.run()
.listen({
onRejected:err=>console.log(err),
onResolved:value=>{
console.log(value);
}
})
Pointed函子
- Pointed函子是实现了of静态方法的函子
- of方法是为了避免使用new来创建对象,更深层次的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值)
Monad函子
- Monad函子是可以变扁的Pointed函子,IO(IO(x))
- 一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad
// Monad函子
const fp = require('lodash/fp')
const fs = require('fs')
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 (val) {
return new IO(function () {
console.log('print', val);
return val
})
}
let toUpper=item=>item.toUpperCase()
let r=readFile('z-myfile.json')
.map(toUpper)
.flatMap(print)
.join()
console.log(r);