slide 地址javascript
如下内容主要参考自 Professor Frisby Introduces Composable Functional JavaScripthtml
假设有个函数,能够接收一个来自用户输入的数字字符串。咱们须要对其预处理一下,去除多余空格,将其转换为数字并加一,最后返回该值对应的字母。代码大概长这样...java
const nextCharForNumStr = (str) =>
String.fromCharCode(parseInt(str.trim()) + 1)
nextCharForNumStr(' 64 ') // "A"
复制代码
因缺思厅,这代码嵌套的也太紧凑了,看多了“老阔疼”,赶忙重构一把...git
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...github
为了这些只用一次的中间变量还要去想或者去查翻译,也是容易“老阔疼”,再改再改~编程
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个小函数。json
这样一来不再用去想中间变量的名称到底叫什么,并且每一步作的事情十分的清晰,一眼就能够看出这段代码在干吗。api
咱们将本来的字符串变量 str 放在数组中变成了 [str],这里就像放在一个容器里同样。数组
代码是否是感受好 door~~ 了?promise
不过在这里咱们能够更进一步,让咱们来建立一个新的类型 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 函数并遵照一些特定规则的容器类型。
那么这些特定的规则具体是什么咧?
fx.map(f).map(g) === fx.map(x => g(f(x)))
复制代码
这其实就是函数组合...
const id = x => x
fx.map(id) === id(fx)
复制代码
假设如今有个需求:获取对应颜色的十六进制的 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
的好处,那就是能够放心地对于这种类型的数据进行任何操做,而不是在每一个函数里面当心翼翼地进行参数检查。
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
)在一块儿罢了。
定义一:对于非空集合 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(false)
复制代码
最后,让咱们对于字符串建立一个新的半群 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'], } */
复制代码
幺半群是一个存在单位元(幺元)的半群。
半群咱们都懂,不过啥是单位元?
单位元:对于半群 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a
举例来讲,对于数字加法这个半群来讲,0就是它的单位元,因此 <Number, +, 0>
就构成一个幺半群。同理:
<Number, *>
来讲单位元就是 1<Boolean, &&>
来讲单位元就是 true<Boolean, ||>
来讲单位元就是 false<Number, Min>
来讲单位元就是 Infinity<Number, Max>
来讲单位元就是 -Infinity那么 <String, First>
是幺半群么?
显然咱们并不能找到这样一个单位元 e 知足
First(e).concat(First('steve')) === First('steve').concat(First(e))
这就是上一节留的小悬念,为什么会感受 First 与 Sum 和 All 不太同样的缘由。
格叽格叽,这二者有啥具体的差异么?
其实看到幺半群的第一反应应该是默认值或初始值,例如 reduce 函数的第二个参数就是传入一个初始值或者说是默认值。
// sum
const Sum = (x) => ({ ... })
Sum.empty = () => Sum(0) // 单位元
const sum = xs => xs.reduce((acc, cur) => acc + cur, 0)
sum([1, 2, 3]) // 6
sum([]) // 0,而不是报错!
// all
const All = (x) => ({ ... })
All.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!!!
复制代码
从以上代码能够看出幺半群比半群要安全得多,
在上一节中幺半群的使用代码中,若是传入的都是幺半群实例而不是原始类型的话,你会发现其实都是一个套路...
const Monoid = (x) => ({ ... })
const monoid = xs => xs.reduce(
(acc, cur) => acc.concat(cur), // 使用 concat 结合
Monoid.empty() // 传入幺元
)
monoid([Monoid(a), Monoid(b), Monoid(c)]) // 传入幺半群实例
复制代码
因此对于思惟高度抽象的函数式来讲,这样的代码确定是须要继续重构精简的~
在讲解如何重构以前,先介绍两个炒鸡经常使用的不可变数据结构:List
、Map
。
顾名思义,正好对应原生的 Array
和 Object
。
由于 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)
复制代码
注意到 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)
复制代码
下面咱们要来实现一个新容器 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
复制代码
有了上一节中 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
复制代码
接下来让咱们作一个发射飞弹的程序~
const lauchMissiles = () => (
// 和 promise 很像,不过 promise 会当即执行
// 并且参数的位置也相反
new Task((rej, res) => {
console.log('lauchMissiles')
res('missile')
})
)
// 继续对以前的任务添加后续操做(duang~给飞弹加特技!)
const app = lauchMissiles()
.map(x => x + '!')
// 这时才执行(发射飞弹)
app.fork(showErr, showSuc)
复制代码
上面的代码乍一看好像没啥用,只不过是把待执行的代码用函数包起来了嘛,这还能吹上天?
还记得前面章节说到的反作用么?虽说使用纯函数是没有反作用的,可是平常项目中有各类必须处理的反作用。
因此咱们将有反作用的代码给包起来以后,这些新函数就都变成了纯函数,这样咱们的整个应用的代码都是纯的~,而且在代码真正执行前(fork
前)还能够不断地 compose
别的函数,为咱们的应用不断添加各类功能,这样一来整个应用的代码流程都会十分的简洁漂亮。
如下代码作了 3 件事:
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}`)
)
复制代码
代码一目了然,按照线性的前后顺序完成了任务,而且在其中还能够随意地插入或修改需求~
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)
进行求值。假如这是两个异步任务,那么彻底没法并行执行。
别慌,吃口药~
下面介绍下主角: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))
复制代码
因为平常编写代码的时候直接用 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 = ...
复制代码
// 伪装是个 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)
复制代码
// 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'))
复制代码
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
复制代码
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
方法第一个参数是建立函子的函数,第二个参数是要应用在函子上的函数。
其实以上代码有 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
方法执行它。
天然变换就是一个函数,接受一个函子(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
并不知足这个规则。因此任何知足这个规则的函数都是天然变换。
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,
)
复制代码
同构是在数学对象之间定义的一类映射,它能揭示出在这些对象的属性或者操做之间存在的关系。
简单来讲就是两种不一样类型的对象通过变形,保持结构而且不丢失数据。
具体怎么作到的呢?
其实同构就是一对儿函数:to
和 from
,遵照如下规则:
to(from(x)) === x
from(to(y)) === y
复制代码
这其实说明了这两个类型都可以无损地保存一样的信息。
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...
复制代码
[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)
复制代码
以上 to be continued...