为什么看不懂?

在掘金课程<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 作者:牙叔教程

(0)
上一篇 2023年4月2日 上午10:52
下一篇 2023年4月2日 上午11:02

相关推荐

发表回复

登录后才能评论