内容简介:以下内容主要参考自假设有个函数,可以接收一个来自用户输入的数字字符串。我们需要对其预处理一下,去除多余空格,将其转换为数字并加一,最后返回该值对应的字母。代码大概长这样...因缺思厅,这代码嵌套的也太紧凑了,看多了“老阔疼”,赶紧重构一把...
以下内容主要参考自 Professor Frisby Introduces Composable Functional JavaScript
4.1.容器(Box)
假设有个函数,可以接收一个来自用户输入的数字字符串。我们需要对其预处理一下,去除多余空格,将其转换为数字并加一,最后返回该值对应的字母。代码大概长这样...
const nextCharForNumStr = (str) => String.fromCharCode(parseInt(str.trim()) + 1) nextCharForNumStr(' 64 ') // "A" 复制代码
因缺思厅,这代码嵌套的也太紧凑了,看多了“老阔疼”,赶紧重构一把...
const nextCharForNumStr = (str) => { const trimmed = str.trim() const number = parseInt(trimmed) const nextNumber = number + 1 return String.fromCharCode(nextNumber) } nextCharForNumStr(' 64 ') // 'A' 复制代码
很显然,经过之前内容的熏(xi)陶(nao),一眼就可以看出这个修订版代码很不 Pointfree...
为了这些只用一次的中间变量还要去想或者去查翻译,也是容易“老阔疼”,再改再改~
const nextCharForNumStr = (str) => [str] .map(s => s.trim()) .map(s => parseInt(s)) .map(i => i + 1) .map(i => String.fromCharCode(i)) nextCharForNumStr(' 64 ') // ['A'] 复制代码
这次借助数组的 map 方法,我们将必须的4个步骤拆分成了4个小函数。
这样一来再也不用去想中间变量的名称到底叫什么,而且每一步做的事情十分的清晰,一眼就可以看出这段代码在干嘛。
我们将原本的字符串变量 str 放在数组中变成了 [str],这里就像放在一个容器里一样。
代码是不是感觉好 door~~ 了?
不过在这里我们可以更进一步,让我们来创建一个新的类型 Box。我们将同样定义 map 方法,让其实现同样的功能。
const Box = (x) => ({ map: f => Box(f(x)), // 返回容器为了链式调用 fold: f => f(x), // 将元素从容器中取出 inspect: () => `Box(${x})`, // 看容器里有啥 }) const nextCharForNumStr = (str) => Box(str) .map(s => s.trim()) .map(i => parseInt(i)) .map(i => i + 1) .map(i => String.fromCharCode(i)) .fold(c => c.toLowerCase()) // 可以轻易地继续调用新的函数 nextCharForNumStr(' 64 ') // a 复制代码
此外创建一个容器,除了像函数一样直接传递参数以外,还可以使用静态方法 of
。
函数式编程一般约定,函子有一个 of 方法,用来生成新的容器。
Box(1) === Box.of(1) 复制代码
其实这个 Box
就是一个函子(functor),因为它实现了 map
函数。当然你也可以叫它 Mappable
或者其他名称。
不过为了保持与范畴学定义的名称一致,我们就站在巨人的肩膀上不要再发明新名词啦~(后面小节的各种奇怪名词也是来源于数学名词)。
functor 是实现了 map 函数并遵守一些特定规则的容器类型。
那么这些特定的规则具体是什么咧?
** 1. 规则一:**
fx.map(f).map(g) === fx.map(x => f(g)(x)) 复制代码
这其实就是函数组合...
** 2. 规则二:**
const id = x => x fx.map(id) === id(fx) 复制代码
4.2.Either / Maybe
假设现在有个需求:获取对应颜色的十六进制的 RGB 值,并返回去掉 #
后的大写值。
const findColor = (name) => ({ red: '#ff4444', blue: '#3b5998', yellow: '#fff68f', })[name] const redColor = findColor('red') .slice(1) .toUpperCase() // FF4444 const greenColor = findColor('green') .slice(1) .toUpperCase() // Uncaught TypeError: // Cannot read property 'slice' of undefined 复制代码
以上代码在输入已有颜色的 key
值时运行良好,不过一旦传入其他颜色就会报错。咋办咧?
暂且不提条件判断和各种奇技淫巧的错误处理。咱们来先看看函数式的解决方案~
函数式将错误处理抽象成一个 Either
容器,而这个容器由两个子容器 Right
和 Left
组成。
// Either 由 Right 和 Left 组成 const Left = (x) => ({ map: f => Left(x), // 忽略传入的 f 函数 fold: (f, g) => f(x), // 使用左边的函数 inspect: () => `Left(${x})`, // 看容器里有啥 }) const Right = (x) => ({ map: f => Right(f(x)), // 返回容器为了链式调用 fold: (f, g) => g(x), // 使用右边的函数 inspect: () => `Right(${x})`, // 看容器里有啥 }) // 来测试看看~ const right = Right(4) .map(x => x * 7 + 1) .map(x => x / 2) right.inspect() // Right(14.5) right.fold(e => 'error', x => x) // 14.5 const left = Left(4) .map(x => x * 7 + 1) .map(x => x / 2) left.inspect() // Left(4) left.fold(e => 'error', x => x) // error 复制代码
可以看出 Right
和 Left
相似于 Box
:
- 最大的不同就是
fold
函数,这里需要传两个回调函数,左边的给Left
使用,右边的给Right
使用。 - 其次就是
Left
的map
函数忽略了传入的函数(因为出错了嘛,当然不能继续执行啦)。
现在让我们回到之前的问题来~
const fromNullable = (x) => x == null ? Left(null) : Right(x) const findColor = (name) => fromNullable(({ red: '#ff4444', blue: '#3b5998', yellow: '#fff68f', })[name]) findColor('green') .map(c => c.slice(1)) .fold( e => 'no color', c => c.toUpperCase() ) // no color 复制代码
从以上代码不知道各位读者老爷们有没有看出使用 Either
的好处,那就是可以放心地对于这种类型的数据进行任何操作,而不是在每个函数里面小心翼翼地进行参数检查。
4.3. Chain
/ FlatMap
/ bind
/ >>=
假设现在有个 json 文件里面保存了端口,我们要读取这个文件获取端口,要是出错了返回默认值 3000。
// config.json { "port": 8888 } // chain.js const fs = require('fs') const getPort = () => { try { const str = fs.readFileSync('config.json') const { port } = JSON.parse(str) return port } catch(e) { return 3000 } } const result = getPort() 复制代码
so easy~,下面让我们来用 Either 来重构下看看效果。
const fs = require('fs') const Left = (x) => ({ ... }) const Right = (x) => ({ ... }) const tryCatch = (f) => { try { return Right(f()) } catch (e) { return Left(e) } } const getPort = () => tryCatch( () => fs.readFileSync('config.json') ) .map(c => JSON.parse(c)) .fold(e => 3000, c => c.port) 复制代码
啊,常规操作,看起来不错哟~
不错你个蛇头...!
以上代码有个 bug
,当 json
文件写的有问题时,在 JSON.parse
时会出错,所以这步也要用 tryCatch
包起来。
但是,问题来了...
返回值这时候可能是 Right(Right(''))
或者 Right(Left(e))
(想想为什么不是 Left(Right(''))
或者 Left(Left(e)))
。
也就是说我们现在得到的是两层容器,就像俄罗斯套娃一样...
要取出容器中的容器中的值,我们就需要 fold
两次...!(若是再多几层...)
因缺思厅,所以聪明机智的函数式又想出一个新方法 chain~,其实很简单,就是我知道这里要返回容器了,那就不要再用容器包了呗。
... const Left = (x) => ({ ... chain: f => Left(x) // 和 map 一样,直接返回 Left }) const Right = (x) => ({ ... chain: f => f(x), // 直接返回,不使用容器再包一层了 }) const tryCatch = (f) => { ... } const getPort = () => tryCatch( () => fs.readFileSync('config.json') ) .chain(c => tryCatch(JSON.parse(c))) // 使用 chain 和 tryCatch .fold( e => 3000, c => c.port ) 复制代码
其实这里的 Left
和 Right
就是单子(Monad),因为它实现了 chain
函数。
monad 是实现了 chain 函数并遵守一些特定规则的容器类型。
在继续介绍这些特定规则前,我们先定义一个 join
函数:
// 这里的 m 指的是一种 Monad 实例 const join = m => m.chain(x => x) 复制代码
- 规则一:
join(m.map(join)) === join(join(m)) 复制代码
- 规则二:
// 这里的 M 指的是一种 Monad 类型 join(M.of(m)) === join(m.map(M.of)) 复制代码
这条规则说明了 map
可被 chain
和 of
所定义。
m.map(f) === m.chain(x => M.of(f(x))) 复制代码
也就是说 Monad
一定是 Functor
Monad
十分强大,之后我们将利用它处理各种副作用。但别对其感到困惑, chain
的主要作用不过将两种不同的类型连接( join
)在一起罢了。
4.4.半群(Semigroup)
定义一:对于非空集合 S,若在 S 上定义了二元运算 ○,使得对于任意的 a, b ∈ S,有 a ○ b ∈ S,则称 {S, ○} 为广群。
定义二:若 {S, ○} 为广群,且运算 ○ 还满足结合律,即:任意 a, b, c ∈ S,有 (a ○ b) ○ c = a ○ (b ○ c),则称 {S, ○} 为半群。
举例来说,JavaScript 中有 concat 方法的对象都是半群。
// 字符串和 concat 是半群 '1'.concat('2').concat('3') === '1'.concat('2'.concat('3')) // 数组和 concat 是半群 [1].concat([2]).concat([3]) === [1].concat([2].concat([3])) 复制代码
虽然理论上对于 <Number, +>
来说它符合半群的定义:
- 数字相加返回的仍然是数字(广群)
- 加法满足结合律(半群)
但是数字并没有 concat 方法
没事儿,让我们来实现这个由 <Number, +>
组成的半群 Sum。
const Sum = (x) => ({ x, concat: ({ x: y }) => Sum(x + y), // 采用解构获取值 inspect: () => `Sum(${x})`, }) Sum(1) .concat(Sum(2)) .inspect() // Sum(3) 复制代码
除此之外, <Boolean, &&>
也满足半群的定义~
const All = (x) => ({ x, concat: ({ x: y }) => All(x && y), // 采用解构获取值 inspect: () => `All(${x})`, }) All(true) .concat(All(false)) .inspect() // All(true) 复制代码
最后,让我们对于字符串创建一个新的半群 First,顾名思义,它会忽略除了第一个参数以外的内容。
const First = (x) => ({ x, concat: () => First(x), // 忽略后续的值 inspect: () => `First(${x})`, }) First('blah') .concat(First('yoyoyo')) .inspect() // First('blah') 复制代码
咿呀哟?是不是感觉这个半群和其他半群好像有点儿不太一样,不过具体是啥又说不上来...?
这个问题留给下个小节。在此先说下这玩意儿有啥用。
const data1 = { name: 'steve', isPaid: true, points: 10, friends: ['jame'], } const data2 = { name: 'steve', isPaid: false, points: 2, friends: ['young'], } 复制代码
假设有两个数据,需要将其合并,那么利用半群,我们可以对 name 应用 First,对于 isPaid 应用 All,对于 points 应用 Sum,最后的 friends 已经是半群了...
const Sum = (x) => ({ ... }) const All = (x) => ({ ... }) const First = (x) => ({ ... }) const data1 = { name: First('steve'), isPaid: All(true), points: Sum(10), friends: ['jame'], } const data2 = { name: First('steve'), isPaid: All(false), points: Sum(2), friends: ['young'], } const concatObj = (obj1, obj2) => Object.entries(obj1) .map(([ key, val ]) => ({ // concat 两个对象的值 [key]: val.concat(obj2[key]), })) .reduce((acc, cur) => ({ ...acc, ...cur })) concatObj(data1, data2) /* { name: First('steve'), isPaid: All(false), points: Sum(12), friends: ['jame', 'young'], } */ 复制代码
4.5.幺半群(Monoid)
幺半群是一个存在单位元(幺元)的半群。
半群我们都懂,不过啥是单位元?
单位元:对于半群 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a
举例来说,对于数字加法这个半群来说,0就是它的单位元,所以 <Number, +, 0>
就构成一个幺半群。同理:
<Number, *> <Boolean, &&> <Boolean, ||> <Number, Min> <Number, Max>
那么 <String, First>
是幺半群么?
显然我们 并不能 找到这样一个单位元 e 满足
First(e).concat(First('steve')) === First('steve').concat(First(e))
这就是上一节留的小悬念,为何会感觉 First 与 Sum 和 All 不太一样的原因。
格叽格叽,这两者有啥具体的差别么?
其实看到幺半群的第一反应应该是 默认值或初始值 ,例如 reduce 函数的第二个参数就是传入一个初始值或者说是默认值。
// sum const Sum = (x) => ({ ... empty: () => Sum(0), // 单位元 }) const sum = xs => xs.reduce((acc, cur) => acc + cur, 0) sum([1, 2, 3]) // 6 sum([]) // 0,而不是报错! // all const All = (x) => ({ ... empty: () => All(true), // 单位元 }) const all = xs => xs.reduce((acc, cur) => acc && cur, true) all([true, false, true]) // false all([]) // true,而不是报错! // first const First = (x) => ({ ... }) const first = xs => xs.reduce(acc, cur) => acc) first(['steve', 'jame', 'young']) // steve first([]) // boom!!! 复制代码
从以上代码可以看出幺半群比半群要 安全 得多,
4.6.foldMap
1.套路
在上一节中幺半群的使用代码中,如果传入的都是幺半群实例而不是原始类型的话,你会发现其实都是一个套路...
const Monoid = (x) => ({ ... }) const monoid = xs => xs.reduce( (acc, cur) => acc.concat(cur), // 使用 concat 结合 Monoid.empty() // 传入幺元 ) monoid([Monoid(a), Monoid(b), Monoid(c)]) // 传入幺半群实例 复制代码
所以对于思维高度抽象的函数式来说,这样的代码肯定是需要继续重构精简的~
2.List、Map
在讲解如何重构之前,先介绍两个炒鸡常用的不可变数据结构: List
、 Map
。
顾名思义,正好对应原生的 Array
和 Object
。
3.利用 List、Map 重构
因为 immutable
库中的 List
和 Map
并没有 empty
属性和 fold
方法,所以我们首先扩展 List 和 Map~
import { List, Map } from 'immutable' const derived = { fold (empty) { return this.reduce((acc, cur) => acc.concat(cur), empty) }, } List.prototype.empty = List() List.prototype.fold = derived.fold Map.prototype.empty = Map({}) Map.prototype.fold = derived.fold // from https://github.com/DrBoolean/immutable-ext 复制代码
这样一来上一节的代码就可以精简成这样:
List.of(1, 2, 3) .map(Sum) .fold(Sum.empty()) // Sum(6) List().fold(Sum.empty()) // Sum(0) Map({ steve: 1, young: 3 }) .map(Sum) .fold(Sum.empty()) // Sum(4) Map().fold(Sum.empty()) // Sum(0) 复制代码
4.利用 foldMap 重构
注意到 map
和 fold
这两步操作,从逻辑上来说是一个操作,所以我们可以新增 foldMap
方法来结合两者。
import { List, Map } from 'immutable' const derived = { fold (empty) { return this.foldMap(x => x, empty) }, foldMap (f, empty) { return empty != null // 幺半群中将 f 的调用放在 reduce 中,提高效率 ? this.reduce( (acc, cur, idx) => acc.concat(f(cur, idx)), empty ) : this // 在 map 中调用 f 是因为考虑到空的情况 .map(f) .reduce((acc, cur) => acc.concat(cur)) }, } List.prototype.empty = List() List.prototype.fold = derived.fold List.prototype.foldMap = derived.foldMap Map.prototype.empty = Map({}) Map.prototype.fold = derived.fold Map.prototype.foldMap = derived.foldMap // from https://github.com/DrBoolean/immutable-ext 复制代码
所以最终版长这样:
List.of(1, 2, 3) .foldMap(Sum, Sum.empty()) // Sum(6) List() .foldMap(Sum, Sum.empty()) // Sum(0) Map({ a: 1, b: 3 }) .foldMap(Sum, Sum.empty()) // Sum(4) Map() .foldMap(Sum, Sum.empty()) // Sum(0) 复制代码
4.7.LazyBox
下面我们要来实现一个新容器 LazyBox
。
顾名思义,这个容器很懒...
虽然你可以不停地用 map
给它分配任务,但是只要你不调用 fold
方法催它执行(就像 deadline
一样),它就死活不执行...
const LazyBox = (g) => ({ map: f => LazyBox(() => f(g())), fold: f => f(g()), }) const result = LazyBox(() => ' 64 ') .map(s => s.trim()) .map(i => parseInt(i)) .map(i => i + 1) .map(i => String.fromCharCode(i)) // 没有 fold 死活不执行 result.fold(c => c.toLowerCase()) // a 复制代码
4.8.Task
1.基本介绍
有了上一节中 LazyBox
的基础之后,接下来我们来创建一个新的类型 Task。
首先 Task
的构造函数可以接收一个函数以便延迟计算,当然也可以用 of
方法来创建实例,很自然的也有 map
、 chain
、 concat
、 empty
等方法。
与众不同的是它有个 fork
方法(类似于 LazyBox
中的 fold
方法,在 fork
执行前其他函数并不会执行),以及一个 rejected
方法,类似于 Left
,忽略后续的操作。
import Task from 'data.task' const showErr = e => console.log(`err: ${e}`) const showSuc = x => console.log(`suc: ${x}`) Task .of(1) .fork(showErr, showSuc) // suc: 1 Task .of(1) .map(x => x + 1) .fork(showErr, showSuc) // suc: 2 // 类似 Left Task .rejected(1) .map(x => x + 1) .fork(showErr, showSuc) // err: 1 Task .of(1) .chain(x => new Task.of(x + 1)) .fork(showErr, showSuc) // suc: 2 复制代码
2.使用示例
接下来让我们做一个发射飞弹的程序~
const lauchMissiles = () => ( // 和 promise 很像,不过 promise 会立即执行 // 而且参数的位置也相反 new Task((rej, res) => { console.log('lauchMissiles') res('missile') }) ) // 继续对之前的任务添加后续操作(duang~给飞弹加特技!) const app = lauchMissiles() .map(x => x + '!') // 这时才执行(发射飞弹) app.fork(showErr, showSuc) 复制代码
3.原理意义
上面的代码乍一看好像没啥用,只不过是把待执行的代码用函数包起来了嘛,这还能吹上天?
还记得前面章节说到的副作用么?虽然说使用纯函数是没有副作用的,但是日常项目中有各种必须处理的副作用。
所以我们将有副作用的代码给包起来之后,这些新函数就都变成了纯函数,这样我们的整个应用的代码都是纯的~,并且在代码真正执行前( fork
前)还可以不断地 compose
别的函数,为我们的应用不断添加各种功能,这样一来整个应用的代码流程都会十分的简洁漂亮。
4.异步嵌套示例
以下代码做了 3 件事:
- 读取 config1.json 中的数据
- 将内容中的 8 替换成 6
- 将新内容写到 config2.json 中
import fs from 'fs' const app = () => ( fs.readFile('config1.json', 'utf-8', (err, contents) => { if (err) throw err const newContents = content.replace(/8/g, '6') fs.writeFile('config2.json', newContents, (err, _) => { if (err) throw err console.log('success!') }) }) ) 复制代码
让我们用 Task 来改写一下~
import fs from 'fs' import Task from 'data.task' const cfg1 = 'config1.json' const cfg2 = 'config2.json' const readFile = (file, enc) => ( new Task((rej, res) => fs.readFile(file, enc, (err, str) => err ? rej(err) : res(str) ) ) ) const writeFile = (file, str) => ( new Task((rej, res) => fs.writeFile(file, str, (err, suc) => err ? rej(err) : res(suc) ) ) ) const app = readFile(cfg1, 'utf-8') .map(str => str.replace(/8/g, '6')) .chain(str => writeFile(cfg2, str)) app.fork( e => console.log(`err: ${e}`), x => console.log(`suc: ${x}`) ) 复制代码
代码一目了然,按照线性的先后顺序完成了任务,并且在其中还可以随意地插入或修改需求~
4.9.Applicative Functor
1.问题引入
Applicative Functor
提供了让不同的函子(functor)互相应用的能力。
为啥我们需要函子的互相应用?什么是互相应用?
先来看个简单例子:
const add = x => y => x + y add(Box.of(2))(Box.of(3)) // NaN Box(2).map(add).inspect() // Box(y => 2 + y) 复制代码
现在我们有了一个容器,它的内部值为局部调用(partially applied)后的函数。接着我们想让它应用到 Box(3)
上,最后得到 Box(5)
的预期结果。
说到从容器中取值,那肯定第一个想到 chain
方法,让我们来试一下:
Box(2) .chain(x => Box(3).map(add(x))) .inspect() // Box(5) 复制代码
成功实现~,BUT,这种实现方法有个问题,那就是单子(Monad)的 执行顺序 问题。
我们这样实现的话,就必须等 Box(2)
执行完毕后,才能对 Box(3)
进行求值。假如这是两个异步任务,那么完全无法并行执行。
别慌,吃口药~
2.基本介绍
下面介绍下主角: ap
~:
const Box = (x) => ({ // 这里 box 是另一个 Box 的实例,x 是函数 ap: box => box.map(x), ... }) Box(add) // Box(y => 2 + y) ,咦?在哪儿见过? .ap(Box(2)) .ap(Box(3)) // Box(5) 复制代码
运算规则
F(x).map(f) === F(f).ap(F(x)) // 这就是为什么 Box(2).map(add) === Box(add).ap(Box(2)) 复制代码
3.Lift 家族
由于日常编写代码的时候直接用 ap 的话模板代码太多,所以一般通过使用 Lift 家族系列函数来简化。
// F 该从哪儿来? const fakeLiftA2 = f => fx => fy => F(f).ap(fx).ap(fy) // 应用运算规则转换一下~ const liftA2 = f => fx => fy => fx.map(f).ap(fy) liftA2(add, Box(2), Box(4)) // Box(6) // 同理 const liftA3 = f => fx => fy => fz => fx.map(f).ap(fy).ap(fz) const liftA4 = ... ... const liftAN = ... 复制代码
4.Lift 应用
- 例1
// 假装是个 jQuery 接口~ const $ = selector => Either.of({ selector, height: 10 }) const getScreenSize = screen => head => foot => screen - (head.height + foot.height) liftA2(getScreenSize(800))($('header'))($('footer')) // Right(780) 复制代码
- 例2
// List 的笛卡尔乘积 List.of(x => y => z => [x, y, z].join('-')) .ap(List.of('tshirt', 'sweater')) .ap(List.of('white', 'black')) .ap(List.of('small', 'medium', 'large')) 复制代码
- 例3
const Db = ({ find: (id, cb) => new Task((rej, res) => setTimeout(() => res({ id, title: `${id}`}), 100) ) }) const reportHeader = (p1, p2) => `Report: ${p1.title} compared to ${p2.title}` Task.of(p1 => p2 => reportHeader(p1, p2)) .ap(Db.find(20)) .ap(Db.find(8)) .fork(console.error, console.log) // Report: 20 compared to 8 liftA2 (p1 => p2 => reportHeader(p1, p2)) (Db.find(20)) (Db.find(8)) .fork(console.error, console.log) // Report: 20 compared to 8 复制代码
4.10.Traversable
1.问题引入
import fs from 'fs' // 详见 4.8. const readFile = (file, enc) => ( new Task((rej, res) => ...) ) const files = ['a.js', 'b.js'] // [Task, Task],我们得到了一个 Task 的数组 files.map(file => readFile(file, 'utf-8')) 复制代码
然而我们想得到的是一个包含数组的 Task([file1, file2])
,这样就可以调用它的 fork
方法,查看执行结果。
为了解决这个问题,函数式编程一般用一个叫做 traverse
的方法来实现。
files .traverse(Task.of, file => readFile(file, 'utf-8')) .fork(console.error, console.log) 复制代码
traverse
方法第一个参数是创建函子的函数,第二个参数是要应用在函子上的函数。
2.实现
其实以上代码有 bug
...,因为数组 Array 是没有 traverse
方法的。没事儿,让我们来实现一下~
Array.prototype.empty = [] // traversable Array.prototype.traverse = function (point, fn) { return this.reduce( (acc, cur) => acc .map(z => y => z.concat(y)) .ap(fn(cur)), point(this.empty) ) } 复制代码
看着有点儿晕?
不急,首先看代码主体是一个 reduce
,这个很熟了,就是从左到右遍历元素,其中的第二个参数传递的就是幺半群(monoid)的单位元(empty)。
再看第一个参数,主要就是通过 applicative functor
调用 ap
方法,再将其执行结果使用 concat
方法合并到数组中。
所以最后返回的就是 Task([foo, bar])
,因此我们可以调用 fork
方法执行它。
4.11.自然变换(Natural Transformations)
1.基本概念
自然变换就是一个函数,接受一个函子(functor),返回另一个函子。看看代码熟悉下~
const boxToEither = b => b.fold(Right) 复制代码
这个 boxToEither
函数就是一个自然变换(nt),它将函子 Box
转换成了另一个函子 Either
。
那么我们用 Left
行不行呢?
答案是不行!
因为自然变换不仅是将一个函子转换成另一个函子,它还满足以下规则:
nt(x).map(f) == nt(x.map(f)) 复制代码
举例来说就是:
const res1 = boxToEither(Box(100)) .map(x => x * 2) const res2 = boxToEither( Box(100).map(x => x * 2) ) res1 === res2 // Right(200) 复制代码
即先对函子 a
做改变再将其转换为函子 b
,是等价于先将函子 a
转换为函子 b
再做改变。
显然, Left
并不满足这个规则。所以任何满足这个规则的函数都是 自然变换 。
2.应用场景
1.例1:得到一个数组小于等于 100 的最后一个数的两倍的值
const arr = [2, 400, 5, 1000] const first = xs => fromNullable(xs[0]) const double = x => x * 2 const getLargeNums = xs => xs.filter(x => x > 100) first( getLargeNums(arr).map(double) ) 复制代码
根据自然变换,它显然和 first(getLargeNums(arr)).map(double)
是等价的。但是后者显然性能好得多。
再来看一个更复杂一点儿的例子:
2.例2:找到 id 为 3 的用户的最好的朋友的 id
// 假 api const fakeApi = (id) => ({ id, name: 'user1', bestFriendId: id + 1, }) // 假 Db const Db = { find: (id) => new Task( (rej, res) => ( res(id > 2 ? Right(fakeApi(id)) : Left('not found') ) ) ) } 复制代码
// Task(Either(user)) const zero = Db.find(3) // 第一版 // Task(Either(Task(Either(user)))) ??? const one = zero .map(either => either .map(user => Db .find(user.bestFriendId) ) ) .fork( console.error, either => either // Either(Task(Either(user))) .map(t => t.fork( // Task(Either(user)) console.error, either => either .map(console.log), // Either(user) )) ) 复制代码
这是什么鬼???
肯定不能这么干...
// Task(Either(user)) const zero = Db.find(3) // 第二版 const two = zero .chain(either => either .fold(Task.rejected, Task.of) // Task(user) .chain(user => Db .find(user.bestFriendId) // Task(Either(user)) ) .chain(either => either .fold(Task.rejected, Task.of) // Task(user) ) ) .fork( console.error, console.log, ) 复制代码
第二版的问题是多余的嵌套代码。
// Task(Either(user)) const zero = Db.find(3) // 第三版 const three = zero .chain(either => either .fold(Task.rejected, Task.of) // Task(user) ) .chain(user => Db .find(user.bestFriendId) // Task(Either(user)) ) .chain(either => either .fold(Task.rejected, Task.of) // Task(user) ) .fork( console.error, console.log, ) 复制代码
第三版的问题是多余的重复逻辑。
// Task(Either(user)) const zero = Db.find(3) // 这其实就是自然变换 // 将 Either 变换成 Task const eitherToTask = (e) => ( e.fold(Task.rejected, Task.of) ) // 第四版 const four = zero .chain(eitherToTask) // Task(user) .chain(user => Db .find(user.bestFriendId) // Task(Either(user)) ) .chain(eitherToTask) // Task(user) .fork( console.error, console.log, ) // 出错版 const error = Db.find(2) // Task(Either(user)) // Task.rejected('not found') .chain(eitherToTask) // 这里永远不会被调用,被跳过了 .chain(() => console.log('hey man')) ... .fork( console.error, // not found console.log, ) 复制代码
4.12.同构(Isomorphism)
同构是在数学对象之间定义的一类映射,它能揭示出在这些对象的属性或者操作之间存在的关系。
简单来说就是两种不同类型的对象经过变形,保持结构并且不丢失数据。
具体怎么做到的呢?
其实同构就是一对儿函数: to
和 from
,遵守以下规则:
to(from(x)) === x from(to(y)) === y 复制代码
这其实说明了这两个类型都能够无损地保存同样的信息。
1. 例如 String
和 [Char]
就是同构的。
// String ~ [Char] const Iso = (to, from) => ({ to, from }) const chars = Iso( s => s.split(''), c => c.join('') ) const str = 'hello world' chars.from(chars.to(str)) === str 复制代码
这能有啥用呢?
const truncate = (str) => ( chars.from( // 我们先用 to 方法将其转成数组 // 这样就能使用数组的各类方法 chars.to(str).slice(0, 3) ).concat('...') ) truncate(str) // hel... 复制代码
2. 再来看看最多有一个参数的数组 [a]
和 Either
的同构关系
// [a] ~ Either null a const singleton = Iso( e => e.fold(() => [], x => [x]), ([ x ]) => x ? Right(x) : Left() ) const filterEither = (e, pred) => singleton .from( singleton .to(e) .filter(pred) ) const getUCH = (str) => filterEither( Right(str), x => x.match(/h/ig) ).map(x => x.toUpperCase()) getUCH('hello') // Right(HELLO) getUCH('ello') // Left(undefined) 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 函数式编程之数组的函数式编程
- 函数式编程 – 函数式编程如何影响您的编码风格?
- 纯函数:函数式编程入门
- 深入理解 Java 函数式编程,第 1 部分: 函数式编程思想概论
- 思想交融,Android中的函数式编程(2):什么是函数式编程
- 编程范式 —— 函数式编程入门
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
快速转行做产品经理
李三科 / 华中科技大学出版社 / 2018-6-1 / 39.90
互联网已经进入以产品为中心的时代,不懂技术一样做高薪产品经理。本书将满足你转行、就业、加薪的愿望。 . 作者李三科,互联网资深产品经理。2011年离开传统销售行业进入互联网行业工作,从对产品经理的工作一无所知,到成长为一名年薪几十万的资深产品经理,他对产品经理职业有着深刻的理解,也积累了丰富的学习、工作经验。本书以作者亲身经历为线索,讲解学习产品经理相关知识和工作方法的经验,同时介绍求......一起来看看 《快速转行做产品经理》 这本书的介绍吧!