javascript函数式编程是继面向过程编程和面向对象编程之后的又一种编程思想,在函数式编程思想中,主张函数是一等公民,旨在用函数的方式来抽象现实事物之间的联系。
今天,我们一起来好好了解下函数式编程
目录
1、为什么要学习函数式编程
2、什么是函数式编程
3、函数式编程的特性
3.1、函数是一等公民
3.2、纯函数
3.2.1、纯函数的优点
3.3、函数柯里化
3.3.1、实现自己的curry函数
3.4、函数组合
3.4.1、结合律
3.4.2、lodash函数所产生的问题
3.5、lodash的fp模块
3.6、Pointfree
4、函子
4.1、副作用
4.2、什么是函子
4.3、多种多样函子
4.3.1、MayBe函子
4.3.2、Either函子
4.3.3、IO函子
4.3.4、Task函子
4.3.5、monad函子
5、结尾
总的来说,我觉得函数式编程可以让我们抽象化事物之间关系的运算过程,而更加专注事物的关系本质
上面我们已经说过了,函数式编程是继面向过程编程和面向对象编程之后的又一种编程方式。
简单理解就是:用来描述数据之间关系的函数,例如y=sin(x)
用我的话理解就是:好比我们数学中的方程式,描述着x和y(数据与数据)之间的关系,而这个方程式可以会有很多复杂的计算,比较加减乘除,乘方等等可能都会有。但是一个同样的x的值,必定只会得到一个y的值。而我们将这一系列的复杂计算都抽象到一个函数当作,让我们无须关注它的运算过程。
函数式编程中以函数为中心思想,故将函数视作"一等公民"
而所谓"一等公民":即,将函数看作和其他数据类型拥有平等的地方,也可以被作为值赋值给变量,可以作为函数的参数传给函数,同时可以作为函数的返回值进行返回。也正是因为如何,所以,函数式编程是支持高阶函数的
因为高阶函数定义便是:
满足以上条件之一的函数,便是高阶函数
常用的高阶函数有:
函数式编程中的函数都是以纯函数的形式来编写程序的
那什么是纯函数,满足以下两点的函数即为纯函数
(即:外部的状态可能会带来副作用。故,一旦依赖外部状态,将会使函数充满不确定性,故副作用会让函数变得不纯。
可能带来副作用得来源有:
如:slice和splice
const arr = [1,2,3,4,5,6,7]
console.log(arr.slice(0, 2))
console.log(arr.slice(0, 2))
console.log(arr.splice(0, 2))
console.log(arr.splice(0, 2))
// 输出
[ 1, 2 ]
[ 1, 2 ]
[ 1, 2 ]
[ 3, 4 ]
可见,slice不会更改arr数组本身,且每次同样的参数输出同样的值
而splice会更改原数组,且每次同样的参数输出不同的值
故slice是纯函数,splice是非纯函数
又比如:
const a = 18
function compare (num) {return num > a ? true : false
}
该函数除了参数外,还依赖了外部数据a,故也不是纯函数。因为当a的值也可能会发生变化,一旦a发生变化,该函数,同样的输入可能就会得到不同的结果。
loadsh是一个纯函数的库,它提供了一系列的纯函数。常用的有:
first last toUpper reverse each includes find findIndex
const _ = require('lodash')function getArea (x, y) {console.log('我执行了')return x * y
}const keepAliveArea = _.memoize(getArea)console.log(keepAliveArea(2, 4))
console.log(keepAliveArea(2, 4))
console.log(keepAliveArea(2, 4))
console.log(keepAliveArea(2, 4))
console.log(keepAliveArea(2, 4))
// 输出
我执行了
8
8
8
8
8
可以看出,函数只有第一次被执行了,后面都只是输出了之前计算到的值,而函数没再执行过
memoize实现原理:函数内利用一个map对象来记录了参数与返回值之间的映射关系,{key: value}形式,其实key是参数字符串,value是返回值
function memoize (f) {let cache = {}return function () {let arg_str = JSON.stringify(arguments)cache[arg_str] = cache[arg_str] || f.apply(f, arguments)return cache[arg_str]}
}
什么是柯里化:
简单来说:就是可以将一个多参数得函数拆开来执行。并且保持函数得返回结果不变
拆分原则: (a, b, c)可拆成 (a), (b), ( c) 或者 (a, b) ,( c) 或者 (a), (b,c)。不可拆成(a, c)(b)
lodash提供了一个函数curry,可以将函数转化为柯里化函数。柯里化之后得函数只有在传了全部参数后,才会执行原函数中得代码
const _ = require('lodash')
// 要柯里化的函数
function getSum (x, y, z) {console.log('函数执行了')return x * y * z
}
// 柯里化后的函数
let curried = _.curry(getSum)
// 测试
const a = curried(1) // 参数不全,函数不会执行
const b = a(2) // 参数不全,函数不会执行
console.log(b(3)) // 此时,参数才全了,会执行
console.log(curried(1,2,3))
console.log(curried(1, 2)(3))
console.log(curried(1)(2)(3))
// 输出
函数执行了
6
函数执行了
6
函数执行了
6
函数执行了
6
可以看出,几种执行结果都一样
function curry (func) {return function curriedFn (...args) {// 判断实参和形参的个数,只有个数相同,才代表参数全部传完,才会去调用函数if (args.length < func.length) {return function () {return curriedFn(...at(Array.from(arguments)))}}// 实参和形参个数相同,调用 func,返回结果return func(...args)}
}
总结:
从上面我们可以看出,柯里化函数很容易造成函数层层嵌套得问题,即(洋葱代码)。
函数组合可以帮我们将多个细粒度的函数组合成一个新的函数。调用这个新的函数相当于依次调用了多个函数
理解:
举例:一个字符串str,要先后经过a函数处理,然后将a函数的返回值再给b函数处理,然后再将b函数的返回值给c函数处理,最终才能得到正确的返回值。那么函数组合可以将a,b,c 3个函数的处理过程组合成一个新的函数(假如叫fn),那么后续只需要将str传入fn进行调用,就可以得到正确的值。
const _ = require('lodash')
let str = 'hello word'function a (str) {return _.toUpper(str) // 转化成大写
}function b (str) {return _.first(str) // 取第一个字符串
}function c (str) {return str + 'i' // 拼接i字符串
}
// const aStr = a(str)
// const bStr = b(aStr)
// const cStr = c(bStr)
// console.log(cStr) 这4句代码和下面一句代码是一模一样的
console.log(c(b(a(str))))
// 输出
Hi
上面可以看出,这种调用方式,层层嵌套,非常繁琐。而函数组合正式解决这类问题的。将3个函数的调用过程合并成一个函数,那么最后,只需要调用一个函数就好。
lodash提供了flowRight()方法和flow()方法来对函数来进行组合
const _ = require('lodash')const fn = _.flowRight(c, b, a) // 调用方式是从右往左调用,因此第一个调用的函数要放最后面
console.log(fn(str))
// 输出
Hi
可以看出,组合后的执行结果和上面是一样的
函数组合也可以进行两两组合,如下:
const _ = require('lodash')const fn = _.flowRight(_.flowRight(c, b), a)
console.log(fn(str))
// 输出
Hi
或者
const _ = require('lodash')const fn = _.flowRight(c, _.flowRight(b, a))
console.log(fn(str))
// 输出
Hi
但是不可将 c和 a结合在一起。
函数的组合需要符合结合律
下面我来看一个例子
const _ = require('lodash')let str = 'hello ni hao'function aaa (item, index) {console.log(item)
}const f = _.flowRight(_.map(aaa), _.split(' '))
f(str)
// 执行后,直接报错了
throw new TypeError(FUNC_ERROR_TEXT);
我们的目的很明显,先将字符串按空格分割成数组,然后再map循环,输出数组中的每一项。但是却报错了,这是为什么。
原因是因为lodash的方法,默认第一个参数是操作的数据(数据优先,函数滞后)
那么当我们执行f(str)的时候,内部会优先执行split()方法,此时,会将str传给split,就会覆盖我们传递的‘ ’空格符。map函数原理也是一样。
_.map(arr, function (item, index) {console.log(item, index)
})
_.split(str, ' ')
那如何解决。继续往下看
const _ = require('lodash')
const fp = require('lodash/fp')let str = 'hello ni hao'
console.log(_.split(str, ' ')) // 数据优先 函数滞后
console.log(fp.split(' ', str)) // 函数优先,数据滞后
// 输出
['hello', 'ni', 'hao']
['hello', 'ni', 'hao']
fp模块的好处:fp模块中的方法更利于函数式编程,在一定场景下比lodash中的方法更好用
本质:Pointfree本质上是一种函数式编程风格,可以理解成一个比较优秀的函数式编程。也是一种函数组合的实现。
概念:
/*
* 需求: 将hello word转化成 WORD_HELLO
*/
const fp = require('lodash/fp')let str = 'hello word'// 非pointfree模式
function formatStr (word) {LocaleUpperCase().split(' ').reverse().join('_') // 函数连环调用
}console.log(formatStr(str))// pointfree模式
const fn = fp.flowRight(fp.join('_'), fp.reverse, fp.split(' '), fp.toUpper)
console.log(fn(str))
如上:fn直接抽象化了整个运算过程,且整个过程不需要关系所需要处理的数据。
学习函子前,我们先来回顾一下非纯函数的副作用
前面我们说过,副作用会让函数变得不存
产生副作用得因素有:
作用:
class Functor {constructor(value) {this.value = value // 函子就相当于是个容器,数据原始值始终被存储在函子中}/* * map函数传入一个纯函数,这个纯函数实现对原始数据的转换逻辑* map函数会返回一个新的函数。相当于实现了将一个函子的原始值映射到另一个函子中*/map (fn) { // return new Functor(fn(this.value)) // 返回一个包含转换后的值的函子}
}const r = new Functor(10)
console.log(r)
const s = r.map((val) => val * val)
console.log(s)// 输出
Functor { value: 10 }
Functor { value: 100 }
可以看出,函子就像一个容器,原始数据值始终被包含在函子中。
而map方法实现了将一个函子的原始值映射到另一个函子中
同时,可以看出,函子的使用new的方式,过于像面向对象编程。故一般函子内部会实现一个of的静态方法,用来实例化一个函子。故,我们把上面代码改造一下
class Functor {/* * 静态方法of,用于创建一个函子*/static of (value) {return new Functor(value)}constructor(value) {this.value = value // 函子就相当于是个容器,数据原始值始终被存储在函子中}/* * map函数传入一个纯函数,这个纯函数实现对原始数据的转换逻辑* map函数会返回一个新的函数。相当于实现了将一个函子的原始值映射到另一个函子中*/map (fn) { // return Functor.of(fn(this.value)) // 返回一个包含转换后的值的函子}
}const r = Functor.of(10)
console.log(r)
const s = r.map((val) => val * val)
console.log(s)// 输出
Functor { value: 10 }
Functor { value: 100 }
MayBe函子的作用主要用于控制一定的副作用(主要控制函子传入值为null或者undefined时的问题)。当传入值为null时,map函数中可能会发生不可控的结果,如下
class Functor {static of (value) {return new Functor(value)}constructor(value) {this.value = value}map (fn) { // return Functor.of(fn(this.value))}
}const r = Functor.of(null)
console.log(r)
const s = r.map((val) => val.split(' ')) // 会直接报错
现在,我们来看MayBe函子是如何解决这个问题的
class MayBe {static of (value) {return new MayBe(value)}constructor(value) {this.value = value}/* * 判断原始值为null 或者undefined时,返回一个存储null值得函子*/map (fn) { // return this.isNoThing() ? MayBe.of(null) : MayBe.of(fn(this.value))}isNoThing () {return this.value === null || undefined}
}const r = MayBe.of(null)
console.log(r)
const s = r.map((val) => val.split(' '))// 输出
MayBe { value: null }
Either函子主要用于处理异常代码。
核心机制在于:会定义两个不同得函子,一个用于处理正确得代码逻辑,一个用于当代码出现异常时,记录当前得异常信息
// Left用于处理异常情况,报错异常信息
class Left {static of (value) {return new Left(value)}constructor (value) {this._value = value}map (fn) {return this // 核心,map不对原始数据做任何处理,直接返回当前函子实例}
}
// Right用于处理正确值
class Right {static of (value) {return new Right(value)}constructor (value) {this._value = value}map(fn) {return Right.of(fn(this._value))}
}function parseJSON(json) {try {return Right.of(JSON.parse(json));} catch (e) {return Left.of({ error: e.message});}
}
const r = parseJSON('{aaa: 123}') // 因为输入得不是标准得json字符串,故会报错
console.log(r)
// 输出
Left { _value: { error: 'Unexpected token a in JSON at position 1' } }
当输入正确得json字符串时
const r = parseJSON('{"aaa": 123}') // 因为输入得不是标准得json字符串,故会报错
console.log(r)
// 输出
Right { _value: { aaa: 123 } }
当我们需要用函子来执行一个不纯得操作时,我可以将这个非纯函数存储到IO函子中,然后利用map函数对这个非纯操作进行一系列包装,最终返回。而这个非纯操作得执行,最终还是交还到调用者来进行出来,只有当调用者调用存在在函子中得这个非纯函数时,才会执行这个非纯操作
特点:
const fp = require('lodash/fp')
class IO {static of (x) {return new IO(function () {return x})}constructor (fn) {this.value = fn}map (fn) {// 把当前的 value 和 传入的 fn 组合成一个新的函数return new IO(fp.flowRight(fn, this.value))}
}
let io = IO.of(process).map(p => p.execPath)
console.log(io)
console.log(io.value())
// 输出
IO { value: [Function] } // value中存储的是flowRight组合成的那个函数。可以看出,延迟了函数的调用
C:Program Files
非纯函数还可能存在一个副作用就是异步操作。异步操作可能会形成地狱式的异步回调,而我们函数式编程中则可以利用Task函子来解决异步操作的问题
但是,说Task之前,我们先来了解一个库folktale
folktale 一个标准的函数式编程库
compose合curry合我们前面说函数组合和函数柯里化差不多,我们就不再做过多的陈述,直接看个例子
const { compose, curry } = require('folktale/core/lambda')
const { split, reverse, join} = require('lodash/fp')// 函数柯里化
// 第一个参数是传入函数的参数个数
let f = curry(2, function (x, y) {return x * y
})
console.log(f(1, 2))
console.log(f(1)(2))
// 函数组合
const s = compose(join('-'), split(' '))
console.log(s('hello word'))// 输出
2
2
hello-word
下面我们重点来看 Task函子
folktale里面提供了一个task方法,这个方法的使用和promise很像。
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) ject(err) // 操作失败solve(data) // 操作成功})})
}/*
* readFIle()函数执行完成就得到了一个Task函子,而该函子中存储着resolve的值
* 执行.run()函数可以拿到该函子中存储的值
* 通过listen函数的分别监听操作成功的值和操作失败的值
* 而map函数,可以再我们输出值之前,对函子中的值做一定的转换。故下面是先将读取到的数据进行了转换后,再输出
*/
// 调用 run 执行
readFile('package-lock.json').map(split('n')).map(find(x => x.includes('lockfileVersion'))).run().listen({onRejected: err => {console.log(err)},onResolved: value => {console.log(value)}
})
// 输出
"lockfileVersion": 1,
下面我们附上读取的这个文件的全部内容
说这个函子前,我们先来看一下IO函子所带来的问题。
const fs = require('fs')
const fp = require('lodash/fp')
class IO {static of (x) {return new IO(function () {return x})}constructor (fn) {this._value = fn}map (fn) {// 把当前的 value 和 传入的 fn 组合成一个新的函数return new IO(fp.flowRight(fn, this._value))}
}// 读取文件
let readFile = function (filename) {return new IO(function() {adFileSync(filename, 'utf-8')})
}// 将读取的内容输出
let print = function(x) {return new IO(function() {console.log(x) // 第一个输出return x})
}let cat = fp.flowRight(print, readFile) // 得到一个组合函数,作用是读取文件,并输出读取的内容// 调用这个组合函数
let r = cat('package-lock.json')._value()._value()
console.log(r) // 第二个输出// 输出
IO { _value: [Function] } // 第一个的输出结果// 第二个的输出结果
{"requires": true,"lockfileVersion": 1,"dependencies": {"folktale": {"version": "2.3.2","resolved": ".","integrity": "sha512-+8GbtQBwEqutP0v3uajDDoN64K2ehmHd0cjlghhxh0WpcfPzAIjPA03e1VvHlxL02FVGR0A6lwXsNQKn3H1RNQ=="},"lodash": {"version": "4.17.21","resolved": ".","integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="}}
}
从上面代码我们可以看出,我们定义了两个函数,readFile和print,分别用于读取文件内容,和输出文件内容。而cat是两个函数组合后的函数。那么cat执行的时候,很明显是先调用了readFile,再把调用后的结果传给print。那么我们来细致看一下调用过程:
5. 此时,这个函数一调用,是不是就执行了具体得读取文件得方法啊,最终将文件输出。
执行流程就是这样。如果再多嵌套个几层,那么就会形成._value()._value()._value()…等这个连环调用得形式,这种方式很显然是不太好得。有点地狱回调得感觉。那怎么解决呢。这个时候 monad函子得作用就出来了
那么,什么是monad函子
请看代码
/*
* 核心在于,用join提前处理了map处理完后的返回值。用flatMap来合并map和join。
* 故当map中的组合函数处理完成后,返回的是一个值时,我们就调用map
* 而如果map中的组合函数处理完成后返回的依然是一个函子,那么我们就需要调用flatMap
*/
const fs = require('fs')
const fp = require('lodash/fp')
class IO {_valstatic of(val) {return new IO(function () {return _value})}constructor(fn) {this._value = fn}map(fn) {return new IO(fp.flow(this._value, fn))}join() {return this._value() // 关键函数}flatMap(fn) {return this.map(fn).join() // 相当于合并map和join方法。将每次map处理完后的返回值通过join提前执行._value(), 从而返回实际需要拿到的值}
}
const readFile = function (filename) {return new IO(function () {adFileSync(filename, 'utf-8')})
}
const print = function (x) {return new IO(function () {console.log(x);return x})
}
const fourResult = readFile('package-lock.json')
.flatMap(print)
.join()
console.log(fourResult)
好了,函数式编程就写到这了。感兴趣的欢迎探讨。哈哈
本文发布于:2024-01-30 23:43:19,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170662940423690.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |