在掘金课程<JavaScript 函数式编程实践指南>的第19小节<Monad(单子):“嵌套盒子”问题解法>,
评论区有人说看不懂
也有人说, 妙啊
为什么?
这是因为发生了思维断层,
在作者看来, 他是循循善诱, 一步一步的教小白搞懂知识, 遇到同频的人, 或者思维非常接近的人, 就会说 妙啊,
在小白眼中, 前18节课, 难度都是 1+1=2, 到了第19节课, 突然就来了个乘法, 加速猝不及防, 人仰马翻,
作者往前走一步, 和小白往前走一步, 这中间差了十万八千里, 差了一道天堑鸿沟,
怎么办?
继续往前走啊, 难不成你还能停下来?
明确问题
“嵌套盒子”问题, 这里就是小白开始和大佬出现差距的开始, 哪里嵌套了?
作者举的例子
考虑这样一个函数:它接收一个用户 id 作为入参,用于检查该用户是否在用户列表中。如果是,则取 id 的前三位作为用户的默认昵称,并将昵称和id一起返回;否则,视为异常。
拆解例子
有的人呢? 会纠结怎么会有这样的需求, 可以想一下平时哪些需求可以匹配这个例子?
比如说, 抽奖, 100个人里面抽10个人, 存在可以视为中奖, 然后把10个人的中奖信息显示出来,
有的人认为, 名字长了会很厉害, 比如说在球球大作战里面, 如果他的名字有18个字符, 那么肯定显示不了那么多, 因为一般人的名字就两三个字, 所以要截取字符串, 取前三位;
这个需求就是这样的, 应该可以想通这个需求怎么出现的了吧
接下来作者给了代码
// 这里省略 isExisted 的实现,大家知道它是用来检查 id 存在性的即可
import isExisted from './utils'
const getUser = id => {
if(isExisted(id)) {
return {
id,
nickName: String(id).slice(0, 3)
}
} else {
throw new Error("User not found")
}
}
getUser这个方法, 就是根据文字写的代码, 一般就这样写, 这里没问题.
然后是
借助 Maybe Functor,我们可以简单包装一下这个查找过程:
import isExisted from './utils'
const getUserSafely = id => {
try {
const userInfo = getUser(id)
return Maybe(userInfo)
} catch(e) {
return Maybe(null)
}
}
这里可能会有人看不懂, 那么他需要返回上一节的课程, 再看一遍Maybe;
这里先贴一下Maybe的方法
const isEmpty = x => x === undefined || x === null
const Maybe = x => ({
map: f => isEmpty(x) ? Maybe(null) : Maybe(f(x)),
valueOf: () => x,
inspect: () => `Maybe {${x}}`
})
Maybe最重要的是map方法, 最特殊的是传入参数null, 那么我们打印一下Maybe(null),看他是什么?
const maybeNull= Maybe(null);
console.log(maybeNull);
console.log(`maybeNull.map() = `, maybeNull.map());
console.log(`maybeNull.valueOf() = `, maybeNull.valueOf());
console.log(`maybeNull.inspect() = `, maybeNull.inspect());
{
map: [Function: map],
valueOf: [Function: valueOf],
inspect: [Function: inspect]
}
maybeNull.map() = {
map: [Function: map],
valueOf: [Function: valueOf],
inspect: [Function: inspect]
}
maybeNull.valueOf() = null
maybeNull.inspect() = Maybe {null}
无论Maybe的参数是什么值, map方法返回的永远是一个新的Maybe
isExisted
这里为了验证方便,我实现一个作弊版的 isExisted,这个函数将会在 id 为 3 的倍数时返回 true,在其他情况下返回 false:
const isExisted = id => id % 3 === 0
为什么要写这个isExisted方法, 为什么要是3的倍数?
因为他只是写个教程, 没必要上数据库查询, 之所以是3的倍数, 因为他喜欢3, 你喜欢几就用几, 这个没关系, 不重要.
运行getUserSafely
const res = getUserSafely(1110021)
// 输出 'Maybe {[object Object]}'
res.inspect()
// 输出 {id: 1110021, nickName: '111'}
res.valueOf()
为什么用1110021, 有3个原因
- 他和数据库的表里面的id相似, 一般的id都是一长串的数字
- 能被3整除
- 要展示符合条件的用户
你不想用1110021, 你也可以用别的, 只要 各个位的和能被3整除 就可以, 比如333333333
接下来作者说了一句话
经过这样一番调整后,findUser 函数在任何情况下都会返回一个 Maybe Functor。
重点来了哦, 这里就断片了,
前文从来没有出现 findUser 这个方法, 依照上文推断, 他这里指的应该是 getUserSafely
调用这个 findUser
这时,如果我想要在一个 Maybe Functor 的 map 方法中,调用这个 findUser 方法,比如这样:
const targetUser = {
id: 1100013,
credits: 2000,
level: 20
}
const userContainer = Maybe(targetUser)
const extractUserId = user => user && user.id
const userInfo = userContainer.map(extractUserId)
.map(getUserSafely)
疑问又产生了
为什么map以后还要再map一下?
这里贴一下Maybe的map方法
map: f => isEmpty(x) ? Maybe(null) : Maybe(f(x)),
userContainer.是一个Maybe
userContainer..map(extractUserId)方法返回的是新的Maybe, 他的参数是f(x)的返回值,
此处的f是extractUserId
const extractUserId = user => user && user.id
那么
const userInfo = userContainer.map(extractUserId)
.map(getUserSafely)
就是
const userInfo = Maybe(userId).map(getUserSafely)
从英文单词来看, 没毛病
嵌套
这一波操作下来,最终得到的 userInfo 就会是一个嵌套的 Maybe Functor:
这里也许有人又卡住了, 嵌套又是个啥?
作者是给了截图的, 不过最好自己运行一下
打印 userInfo 的完整代码
const targetUser = {
id: 1100013,
credits: 2000,
level: 20,
};
const isEmpty = (x) => x === undefined || x === null;
const Maybe = (x) => ({
map: (f) => (isEmpty(x) ? Maybe(null) : Maybe(f(x))),
valueOf: () => x,
inspect: () => `Maybe {${x}}`,
});
const userContainer = Maybe(targetUser);
const extractUserId = (user) => user && user.id;
const isExisted = (id) => id % 3 === 0;
const getUser = (id) => {
if (isExisted(id)) {
return {
id,
nickName: String(id).slice(0, 3),
};
} else {
throw new Error("User not found");
}
};
const getUserSafely = (id) => {
try {
const userInfo = getUser(id);
return Maybe(userInfo);
} catch (e) {
return Maybe(null);
}
};
const userInfo = userContainer.map(extractUserId).map(getUserSafely);
console.log(userInfo);
debugger;
最后一行是debugger;
你运行代码的时候, 不要在命令行中写
node index.js
而应该点击vscode的菜单栏中的 运行/ 启动调试, 这样, debugger 才会起作用
运行后, 查看 userInfo
不管是 userInfo 还是 userInfo.valueOf(), 他们都是 Maybe,
valueOf() 我们一般认为是具体的值, 但是此处是 Maybe, 这就是作者说的嵌套
嵌套是看见了, 我们还要去分析代码,
嵌套是怎么发生的
回顾Maybe的方法
const Maybe = (x) => ({
map: (f) => (isEmpty(x) ? Maybe(null) : Maybe(f(x))),
valueOf: () => x,
inspect: () => `Maybe {${x}}`,
});
valueOf 应该返回具体的值, 为什么上面 valueOf 返回的是 Maybe呢?
那么Maybe传入的参数肯定就是Maybe
也就是说
const Maybe = (x) => ({
map: (f) => (isEmpty(x) ? Maybe(null) : Maybe(f(x))),
valueOf: () => x,
inspect: () => `Maybe {${x}}`,
});
Maybe(f(x)) 的f(x) 返回值是一个Maybe,
此处的 f 是 getUserSafely
const getUserSafely = (id) => {
try {
const userInfo = getUser(id);
return Maybe(userInfo);
} catch (e) {
return Maybe(null);
}
};
getUserSafely 返回的确实是Maybe
作者说
对于 map 接收的回调参数 f 来说,f 预期的入参往往是数据本身,而不是一个装着数据的盒子。
他这句话是对的, 但是此处的原因是getUserSafely的返回值是一个Maybe, 我们的重点难道不应该是修改
getUserSafely这个方法吗?
回看文章之前的内容, 作者说
经过这样一番调整后,findUser 函数在任何情况下都会返回一个 Maybe Functor。
为什么 getUserSafely 要返回 Maybe?
不返回Maybe, 不就没问题了吗?
const getUserSafely = (id) => {
try {
const userInfo = getUser(id);
return userInfo;
} catch (e) {
return null;
}
};
我觉得这里是一个真的思维断层, 有必要返回Maybe吗
按下不表, 继续往下看
作者又给了一个非线性计算场景下的嵌套 Functor 的例子
把
// 该函数将对给定 score 作权重为 high 的计算处理
const highWeights = score => score*0.8
// 该函数将对给定 score 作权重为 low 的计算处理
const lowWeights = (score) => score*0.5
const computeFinalScore = (generalScore, healthScore) => {
const finalGeneralScore = highWeights(generalScore)
const finalHealthScore = lowWeights(healthScore)
return finalGeneralScore + finalHealthScore
}
改成了
const computeFinalScore = (generalScore, healthScore) =>
Identity(highWeights(generalScore))
.map(
finalGeneralScore =>
Identity(lowWeights(healthScore))
.map(
finalhealthScore =>
finalGeneralScore + finalhealthScore
)
)
越改越复杂, 完全没有美感可言
继续往下阅读
……
总结一下就是: 兵来将挡, 水来土掩
flatMap 和 map 其实很像,区别在于他们对回调函数 f(x) 的预期:
map 预期 f(x) 会输出一个具体的值。这个值会作为下一个“基础行为”的回调入参传递下去。
而 flatMap 预期 f(x) 会输出一个 Functor,它会像剥洋葱一样,把 Functor 里包裹的值给“剥”出来。确保最终传递给下一个“基础行为”的回调入参,仍然是一个具体的值。
如果是具体的值, 就调用map方法;
如果是盒子, 就调用flatMap方法;
碰到有需要两次 valueOf 方法的地方, 就把map改成flatMap;
这个还需要自己判断参数是不是盒子,
应该再加一个方法
autoFlatMap()
我觉得, getUserSafely 没必要返回 Maybe, 所有问题都是因他的返回值而产生的;
这是一个强行嵌套的教程, 读下来, 没有那种落花流水, 水到渠成的感觉;
不过教程整体质量还是不错的, 对于 rxjs 新手还是挺有帮助的
原文链接:https://juejin.cn/post/7216917459009093692 作者:牙叔教程