这篇文章是在学习函数式编程时的学习笔记,里面有很多自己对函数式编程的理解,有些理解可能不一定准确,希望对大家学习函数式编程有些参考价值,有不对的地方也请大家指出
一、什么是函数式编程?
函数式编程是一种编程思想,与面向对象编程平级。
面向对象的编程思想是把现实世界的事物抽象成类和对象
函数式编程思想是把运算过程抽象成一个函数
这里的函数不是指代码中的函数,而是数学中的函数,y = sin(x),这里的x和y通过sin函数就绑定了一种映射关系,输入x,通过sin函数的运算(处理)就会得到一个y的值
1、函数是一等公民
函数可以存储在变量中
函数可以作为参数传递
函数可以作为值返回
2、高阶函数
可以把函数作为参数传递给另一个函数
可以把函数作为另一个函数的结果返回
function once (fn) {
let done = false
return function () {
if (!done) {
done = true
return fn.apply(this, arguments)
}
}
}
3、使用高阶函数的意义
高阶函数屏蔽调了处理细节,只需要关注最终结果
高阶函数用来抽象通用的问题
// 面向过程
let arry = [1,2,3,4]
for (let i=0; i<arry.length; i++) {
// 此处对arry进行一些处理
console.log(arry[i])
}
// 函数式编程
let arry2 = [1,2,3,4]
// lodash内的forEach函数
forEach(arry2, (item)=>{
console.log(item)
})
上面的例子中可以看到,使用函数式编程,数据和处理细节是分开的,我们不需要关心如何循环这个数组,只需要传递一个函数对每一项进行处理,常用的高阶函数有:
forEach
map
filter
every
some find/findIndex reduce
sort
4、纯函数
上面的函数始终都会有一个返回值,纯函数指的是相同的输入,始终会有相同的输出
用数组的slice和splice举例
let arr = [1,2,3,4,5,6,7]
// slice是纯函数,每次调用都会返回相同的值
arr.slice(0,2) //1,2
arr.slice(0,2) //1,2
// splice是不纯函数,每次调用都会改变原数组,返回的值不一样
arr.splice(0,2) //1,2
arr.splice(0,2) //3,4
纯函数的好处
可缓存:由于纯函数具有相同的输入具有相同的输出的特性,如果一个函数在参数相同的情况下要执行多次,只需把第一次的结果缓存起来,后面调用的时候会直接返回结果。lodash有memoize函数,可直接调用,手动实现原理如下:
function getArea (r) {
console.log(r)
return Math.PI * r * r
}
function memoize (fn) {
let catchObj = {}
return function () {
let key = JSON.stringify(arguments)
catchObj[key] = catchObj[key] || fn.apply(fn, arguments)
return catchObj[key]
}
}
const getAreaMemory = memoize(getArea)
console.log(getAreaMemory(2))
console.log(getAreaMemory(2))
console.log(getAreaMemory(2))
console.log(getAreaMemory(2))
上面代码中,后面即使多次调用getAreaMemory方法,也不会执行Math.PI * r * r的运算
可测试:由于纯函数都有返回值,且参数不变,返回值统一,便于写测试用例
并行处理:由于纯函数不会操作共享的内存,所以在并行处理的时候不会产生冲突
副作用
先看下面的例子
//非纯函数
let min = 20;
function compare (num) {
return num >= min
}
上面代码中,函数的返回值依赖外部的min的值,相同的输入不一定有相同的输出,为了解决这个问题,有两个方案;一个是将变量min放在函数内部
function compare (num) {
let min = 20;
return num >= min
}
还有一个就是函数的柯里化
function compare (min) {
return funciton (num) {
return num >= min
}
}
5、函数柯里化
我对函数柯里化的理解是,一个函数接受很多参数,先传递部分参数调用,返回一个函数接受另一部分参数,最终返回结果,例如loadsh中的柯里化函数
const _ = require('lodash') // 要柯里化的函数
function getSum (a, b, c) {
return a + b + c
}
// 柯里化后的函数
let curried = _.curry(getSum) // 测试
curried(1, 2, 3) curried(1)(2)(3)
curried(1, 2)(3)
手动实现柯里化函数如下:
const curry = function (func) {
return function curriedFn (...args) {
if (args.length < func.length) {
return function (...args2) {
return curriedFn(...args.concat(args2))
}
} else {
return func(...args)
}
}
}
函数柯里化的意义:
使函数参数缓存
让函数的粒度更小
使多元函数变为一元函数,组合这些一元函数产生强大的功能
6、函数组合
洋葱代码:一个函数的入参依赖另一个函数的出参,函数一层套一层
例如:fn1(fn2(fn3()))
函数组合可以规避这个问题的发生,把fn1,fn2,fn3想象成一个三段小管道,将这三个小管道拼接起来,最终组装成一个大管道,组装后的大管道就是一个新函数,这个函数接受入参,依次经过fn1,fn2,fn3的处理,返回最终的结果,组装的过程就是函数组合,lodash内置函数组合函数,例如将一个数组先翻转,然后提取数组第一项,最后将第一项转为大写:
const _ = require('lodash')
const reverse = value => value.reverse()
const toUper = value => value.toUpperCase()
const first = value => value[0]
const fn = _.flowRight(toUper, first, reverse);
console.log(fn(['one', 'two', 'three'])) //THREE
flowRight是lodash内置的函数组合函数,内部实现原理如下:
const reverse = value => value.reverse()
const toUper = value => value.toUpperCase()
const first = value => value[0]
function compose (...fnLists) {
return function (value) {
return fnLists.reverse().reduce((acc, fn)=>{
return fn(acc)
}, value)
}
}
const fn2 = compose(toUper, first, reverse);
console.log(fn2(['one', 'two', 'three'])) //THREE
函数的组合要满足结合律
7、Point Free
Point Free:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
不需要指明处理的数据
只需要合成运算过程
需要定义一些辅助的基本运算函数
let fp = require('lodash/fp')
// web world with --> W. W. W.
const firstLetterToupper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
console.log(firstLetterToupper('web world with'))
8、函子
容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运 行一个函数对值进行处理(变形关系)
我理解的容器就是把函数想象成一个容器盒子,入参一个数值,经过该容器盒子的处理,返回一个最终的结果,而函子是具有map、of方法的容器,通过of方法入参一个值,通过map方法传入对这个值的处理函数,map方法执行后会返回一个包含新值的函子,可以继续调用map方法对上一map方法返回的值进行处理,直到得到最终想要的结果,下面是一个例子:
// 函子
class Container {
static of (value) {
return new Container(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return Container.of(fn(this._value))
}
}
const r = Container.of(5)
.map((x)=>x+1)
console.log(r)
调用of方法省去了new Container这一步,执行of方法后,参数5的这个值就被存储到了对象内部_value上,调用map方法时,会将该值当做入参传递给map的入参函数内
函子有很多种
处理空值的MayBe 函子
// maybe函子
class Maybe {
static of (val) {
return new Maybe(val)
}
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
}
}
const r2 = Maybe.of(5).map(x=>x+1)
const r3 = Maybe.of('hello').map(x=>x+'1').map(x=>null).map(x=>x.split(' '))
console.log(r2,r3) //Maybe { _value: 6 } Maybe { _value: null }
处理异常的either函子
// eithier函子
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))
}
}
function parseStr (str) {
try {
return Right.of(JSON.parse(str))
} catch (err) {
return Left.of({error: err.message})
}
}
const r4 = parseStr('{ "name": "zs" }').map(x=>x.name.toUpperCase())
console.log(r4)
IO函子
IO 函子中的 _value 是一个函数,这里是把函数作为值来处理
IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行)
把不纯的操作交给调用者来处理
const fp = require('lodash/fp')
// IO函子
class IO {
static of (value) {
return new IO(()=>{
return value
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
const _io = IO.of(process).map(p=>p.execPath).map(k=>{
return k.toUpperCase()
})
console.log(_io._value())
这个例子中,_io并不是一个执行结果,而是一个拥有执行函数的IO函子,操作者手动调用_value()时,函数才会执行
Monad(单子)
在使用 IO 函子的时候,如果我们写出如下代码:
const fs = require('fs')
const fp = require('lodash/fp')
let readFile = function (filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8')
})
}
let print = function(x) {
return new IO(function() {
console.log(x)
return x })
}
// IO(IO(x))
let cat = fp.flowRight(print, readFile)
// 调用
let r = cat('package.json')._value()._value()
console.log(r)
此时readFile返回的IO函子会作为入参传递给print,print返回一个IO函子
Monad 函子是可以变扁的 Pointed 函子,IO(IO(x))
一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad
const fs = require('fs')
const fp = require('lodash/fp')
class IO {
static of (value) {
return new IO(()=>{
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()
}
}
function readFile (fileNmae) {
return new IO(()=>{
return fs.readFileSync(fileNmae, 'utf-8')
})
}
function print (x) {
return new IO(()=>{
return x
})
}
const r = readFile('package.json')
.map(fp.toUpper)
.flatMap(print)
.join()
console.log(r)
通过内部的join方法,多调用了一次_value()
函子这一部分有些抽象,理解的不够深入,可能在实际开发过程中会有不一样的体会,大家共勉