发明单曲

2020-08-30 03:38:45

最近我参加了一场关于单子星的讨论。在搜索一些要共享的资源时,我意识到大多数文章都使用类型签名和规则来解释它们。我认为,要弄懂他们,缺少的一个要素是理解他们背后的直觉。你怎么能最终发明单曲呢?

好的,让我们试着建立这种直觉。我们将避免使用类型和范畴论。

假设您有几个函数来获取用户、配置文件和显示图片:

但是,这会抛出一个错误:所有这些函数都可能返回NULL。例如,如果getUser返回NULL,您将看到:

函数getDisplayPictureFromId(Id){const user=getUser(Id)if(!user)return const profile=getProfile(User)if(!profile)return return getDisplayPicture(Profile)}。

但是,这件事变得相当丑陋了。所有这些条件返回都会分散您真正想要做的事情。如果有一种方法可以去掉这些条件句呢?

所以,这就是我们的挑战:让我们去掉这些条件句。我们可以做到这一点的一种方法,就是做一些帮手。这个帮助器将让我们将这些函数链接在一起:

.whenExists仅在该值存在的情况下才会运行回调。你可以这样写:

类链接器{构造函数(V){this.value=v}当Exists(F){if(!this.value)返回this;返回新链接器(f(this.value))}}。

啊哦。现在,我们在链接器内部有了链接器。理想情况下,我们需要一个以某种方式将这些链接器“合并”在一起的函数。

Class Chainer{构造函数(V){this.value=v}当Exists(F){if(!this.value)返回this;返回new Chainer(f(this.value))}当ExistsMerge(F){if(!this.value)返回this;返回f(this.value)}}。

瞧,你刚刚发明了一种特殊类型的单子。有点(1)。Chainer类似于“可能的单子”。当Exists类似于其FMAP操作时,以及当ExistsMerge类似于其绑定操作时。如果您现在对基于类型的技术问题感兴趣,请参阅这篇文章。

所以,现在我们找到了这个很酷的Chainer。我们可以就此打住,或者想得更远一点。它有什么特别的吗?

嗯,它就像一个包裹着一些信息的盒子。我们可以使用When Exists和When ExistsMerge与该框进行交互。

FetchUser(id,(err,user)=>;{if(Err)...。FetchProfile(profile,(err,profile)=>;{if(Err)...。FetchDisplayPicture(...}})。

如果我们创建类似异步链接器(Async Chainer)的东西会怎么样:它存储一些未来计算的结果。然后,您可以使用whenExists*函数,该函数允许您在计算时与值进行交互。我们把回调地狱变成了

那么,当现有与之合并时进行替换,那么你就走上了发现Promise的道路,而Promise也是一个单子。有点(2)。

现在,注意到可为空的用例和异步用例都有相同的接口,这是非常酷的。当退出时的名称可能有点太具体了。实际上,它所做的就是为您提供一个接口来映射值。如果您使用单词map,那么当ExistsMerge确实允许您在值上进行平面映射。

这让我们开始了解Monad的基本抽象:一个带有map接口的方框和flatMap。当您更深入地观察时,您会注意到这个抽象可以处理很多其他事情。如果你很好奇,比如研究一下结果单子。

当您经历这一过程时,您可能已经意识到还有其他方法可以解决单体解决的问题。您的语言可以有一个安全的调用抽象,而不是Chainer。您的语言可以有异步/等待抽象,而不是承诺。有时,这些可能是更好的选择。不过,如果您的语言没有这些抽象,您可以使用单体来解决它们。当然,与任何强大的抽象一样,您可以发明新的Monad来简化您的业务逻辑。

(1)我在Chainer上有点作弊,避开子类型Just和Nothing。这使得您不能表达类似于Just(NULL)的内容。尽管如此,它还是切中了要害:)。

(2)同样,Promise也不完全是一个单体,因为它将平板地图和地图与Then混合在一起。这使得你不能许诺<;许诺<;Res>;>;。尽管如此,它还是切中了要害。

感谢Joe Averbukh,Mark Shlick,Daniel Woelfel,Irakli Safareli,Jacky Wang审阅本文草稿