摘要:函数式编程一般约定,函子有一个方法,用来生成新的容器。是实现了函数并遵守一些特定规则的容器类型。定义二若为广群,且运算还满足结合律,即任意,有,则称为半群。
slide 地址
四、Talk is cheap!Show me the ... MONEY!以下内容主要参考自 Professor Frisby Introduces Composable Functional JavaScript4.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 => g(f(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]))
虽然理论上对于
数字相加返回的仍然是数字(广群)
加法满足结合律(半群)
但是数字并没有 concat 方法
没事儿,让我们来实现这个由
const Sum = (x) => ({ x, concat: ({ x: y }) => Sum(x + y), // 采用解构获取值 inspect: () => `Sum(${x})`, }) Sum(1) .concat(Sum(2)) .inspect() // Sum(3)
除此之外,
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"], } */4.5.幺半群(Monoid)
幺半群是一个存在单位元(幺元)的半群。
半群我们都懂,不过啥是单位元?
单位元:对于半群,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a
举例来说,对于数字加法这个半群来说,0就是它的单位元,所以
对于
对于
对于
对于
对于
那么
显然我们并不能找到这样一个单位元 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!!!
从以上代码可以看出幺半群比半群要安全得多,
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()) // a4.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: 22.使用示例
接下来让我们做一个发射飞弹的程序~
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 84.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)参考资料
JS函数式编程指南
Pointfree 编程风格指南
Hey Underscore, You"re Doing It Wrong!
Functional Concepts with JavaScript: Part I
Professor Frisby Introduces Composable Functional JavaScript
函数式编程入门教程
What are Functional Programming, Monad, Monoid, Applicative, Functor ??
相关文章JavaScript 函数式编程(一)
JavaScript 函数式编程(二)
JavaScript 函数式编程(三)-- 本文
JavaScript 函数式编程(四)正在酝酿...
以上 to be continued...
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/96872.html
摘要:它大致概述并讨论了前端工程的实践如何学习它,以及在年实践时使用什么工具。目的是每年发布一次内容更新。前端实践第一部分广泛描述了前端工程的实践。对大多数人来说,函数式编程看起来更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 这是任何人都可以用来了解前端开发实践的指南。它大致概述并...
摘要:它大致概述并讨论了前端工程的实践如何学习它,以及在年实践时使用什么工具。目的是每年发布一次内容更新。前端实践第一部分广泛描述了前端工程的实践。对大多数人来说,函数式编程看起来更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 这是任何人都可以用来了解前端开发实践的指南。它大致概述并...
摘要:它大致概述并讨论了前端工程的实践如何学习它,以及在年实践时使用什么工具。目的是每年发布一次内容更新。前端实践第一部分广泛描述了前端工程的实践。对大多数人来说,函数式编程看起来更加自然。 1 Front-End Developer Handbook 2017 地址:https://frontendmasters.com/b... 这是任何人都可以用来了解前端开发实践的指南。它大致概述并...
摘要:子类不是父类实例的问题是由类式继承引起的。所以寄生式继承和构造函数继承的组合又称为一种新的继承方式。但是这里的寄生式继承处理的不是对象,而是类的原型。看上去略微复杂,还得好好研究。 寄生组合式继承(终极继承者) 前面学习了类式继承和构造函数继承组合使用,也就是组合继承,但是这种继承方式有个问题,就是子类不是父类的实例,而子类的原型是父类的实例。子类不是父类实例的问题是由类式继承引起的。...
摘要:函数式编程,一看这个词,简直就是学院派的典范。所以这期周刊,我们就重点引入的函数式编程,浅入浅出,一窥函数式编程的思想,可能让你对编程语言的理解更加融会贯通一些。但从根本上来说,函数式编程就是关于如使用通用的可复用函数进行组合编程。 showImg(https://segmentfault.com/img/bVGQuc); 函数式编程(Functional Programming),一...
摘要:在函数内保存数据在命令式语言中,函数内部的私有变量局部变量是不能被保存的。从程序的执行方式上来讲,局部变量在栈上分配,在函数执行结束后,所占用的栈被释放。这一点其实是破坏它的函数式特性的。 本文内容是我阅读《JavaScript语言精髓与编程实践》时,做的读书笔记,周爱民老师的书写的太深刻了! 函数式语言中的函数 首先要有一个概念:并不是一个语言支持函数,这个语言就可以叫做函数式语言。...
阅读 1798·2021-09-22 15:55
阅读 3492·2021-09-07 10:26
阅读 589·2019-08-30 15:54
阅读 640·2019-08-29 16:34
阅读 813·2019-08-26 14:04
阅读 3212·2019-08-26 11:47
阅读 2101·2019-08-26 11:33
阅读 2261·2019-08-23 15:17