有一篇可爱的老博文《你的功能是什么颜色》(WCIYF)。如果你还没有读过这篇文章,请不要读这篇。
既然你在这里,你应该理解那篇文章中描述的有色函数问题。我想探讨一下这个问题是如何将beyondasync问题概括化的,以及它是如何与备受哀叹的monad联系在一起的。
阅读这篇文章时,你并不需要知道什么是单子,但你应该理解链接文章中描述的有色函数问题。
概括一下WCIYF:Red函数是异步函数。如果需要更新蓝色(同步)函数以调用红色函数,则需要将蓝色函数更改为红色,这反过来会导致红色扩散。而且,调用红色函数更痛苦。我主要关注的是发红的扩散,但我们也会讨论疼痛。
作者提到承诺是一种缓解措施,但最终决定使用线程(绿色或os)是更好的解决方案。
承诺是单子的一个例子。使用它们是痛苦的,但我认为这是值得的:你应该咬紧牙关,把所有东西都染成红色。当所有东西都是红色时,你不必考虑颜色,问题就消失了。
我将给出“红色函数”的几个替代定义,我们将有大致相同的问题,解决方案将始终是一些单子。
WCIYF最大的启示是“红色函数是异步函数”。相反,考虑抛出异常的函数。我们可以考虑那些“红色”吗?如果您有一个不抛出(蓝色)的函数,并且需要对其进行编辑以调用抛出的函数,那么您的函数也将成为抛出的函数,除非您能够处理异常。在实践中,有时你可以处理异常,有时你不能,所以只有当你不能处理异常时,红色才会增加。捕获异常类似于等待承诺,让代码处理异常比等待承诺更合理。然而,让我们关注一下我们无法处理异常的情况,因此红色确实像异步情况一样扩散。
旁注:像Java这样的一些语言已经检查了异常,在这种情况下,您确实需要更新方法签名,并通过声明抛出异常来显式地将bluefunction更改为红色。这更适合异步情况,因为存在实际的(机械的)代码更改。在未检查的情况下,所有的痛苦都落在了被寻呼的人身上,因为在运行时抛出了新的异常。
例外将快乐的道路与悲伤的道路分开,将典型的道路与非典型的道路分开,将平凡的道路与例外的道路分开。它们有自己的控制流机制作为实现细节1。
作为替代方案,许多语言或库也定义了一种类型<;SomeErrorType,DesiredType>;,可以指定函数返回以下两种情况之一。你必须选择你喜欢的错误类型,这与本文无关,所以我们只能说我们选择了“SomeErrorType”。
WCIYF将承诺解释为将回调/错误返回的概念具体化。同样地,也不是<;SomeErrorType,*>;具体化典型/非典型结果的概念。在这两种情况下,函数现在都返回一个表示控制流的值,而值往往比行为更容易推理。
旁注:在我所见过的每一种语言中,承诺/未来都有一个成功和失败的案例。所以它本质上要么是内置的。我认为这是因为有异常的语言通常无法保证异常不会发生。这感觉很像任何东西都可以为空的语言。我会对一种强制使用可选的语言感兴趣,而不是允许null异常。也许是哈斯克尔干的?!
WCIYF有点承认承诺比撤回承诺痛苦,但也不以为然地认为它们是半个解决方案。我只想指出一些语言有for表达式(例如Scala)或do表示法(例如Haskell),这进一步减轻了痛苦2。一个粗略的例子:
这是一些虚构的代码,用于从用户id异步获取用户,然后通过id获取他们的所有朋友。请允许我在所有值之后使用指定的类型(typescript样式)重复代码:
def getFriends(用户ID):承诺<;用户[]>;=对于{user:user<;-asyncGetUser(userId):Promise<;user>;friends:user[]<;-user.friends.traverse(asyncGetUser):Promise<;user[]>;}交朋友
请注意<;-无论什么时候,只要权利是承诺,我们就永远拥有它<;T>;。还要注意承诺的激增,getFriends会返回一个承诺,因为asyncGetUser会。traverse是map和Promise的结合。所有的,并且它做了唯一对这段代码有意义的事情。
现在,假设我们有syncGetUser,它返回<;SomeErrorType,用户>;。代码相同,但类型不同:要么<;SomeErrorType,T>;取代承诺<;T>。
def getFriends(userId):要么<;SomeErrorType,用户[]>;=对于{user:user<;-syncGetUser(userId):或者<;SomeErrorType,用户>;friends:user[]<;-user.friendIds.traverse(asyncGetUser):或者<;SomeErrorType,用户[]>;}交朋友
我将从一个具体的例子开始:在某些上下文中,假设您有一个单页应用程序,其中的微服务都在进行http调用,在浏览器之间和服务之间发送json。在所有日志语句中都包含一个每个请求的“相关id”,这很好。为了使其正常工作,无论何时登录,都需要提供正确的id。它还需要在您对另一个服务进行http调用时可用。假设“红色函数”是需要知道相关id的函数。鉴于我们需要用于日志记录和http调用的id,我们预计红色会迅速扩散到几乎所有地方。
然而,回想一下,我们能够通过返回一个更好地代表我们需求的值来解决其他问题。因此,在这种情况下,我们需要返回一个值,该值表示“对相关id的需求”。为此,我们可以使用一个函数,即。
def getFriends(userId):CorrelationId=>;User[]=对于{User:User<;-syncGetUser(userId):CorrelationId=>;用户朋友:User[]<;-User.friendIds.traverse(asyncGetUser):CorrelationId=>;User[]}生成朋友
请注意,这段代码不处理相关Id(它也不处理异步性或错误)。
既然我们已经三次编写了基本相同的代码,那么让我们把不同的部分作为变量:
def getFriends<;F:Monad>;(userId):F<;用户[]>;=对于{user:user<;-syncGetUser(userId):F<;user>;friends:user[]<;-user.friends.traverse(asyncGetUser):F<;user[]>;}交朋友
在这里,我的方法由F参数化,但有一个限制,即F是monad 4(F:monad)。您可以通过替换F<*>;带着承诺<*>;,要么<;SomeErrorType,*>;,或CorrelationId=>;*。(在*处,你必须替换你真正关心的类型)。
我们甚至可以组合东西,所以F可以是CorrelationId=>;承诺<*>;如果getUser需要一个CorrelationId,我们希望它是异步的。
许多语言都有泛型类型。以阵列为例<;Int>;,我将数组称为外部,Int称为内部。通常,一种语言会让你在一个变量数组中创建内部变量<;T>;是一个数组,但我不知道也不在乎里面是什么。能够在外部设置变量的情况不太常见。F<;Int>;有点奇怪,你真的不知道它是什么,只有一个建议与Int有关。然而,这正是我们三个例子5中的变化。
在我们的示例中,我们需要一个CorrelationId,但相同的模式可以用于您需要的其他事情,但实际上不想显式传递。例如,数据库连接、用户会话ID、用户授权信息。任何时候你想要一些可用的东西,但并不真的想把它传给别人。
一只虫子进来了。用户界面中的数字有误。我的团队负责的后端实际上只是调用其他团队负责的其他API。我们首先要看的是我们对这些外部API的请求和响应(毕竟,如果是上游错误,我们希望尽快推卸责任)。现在,由于各种原因,我们不想记录所有的请求和响应。因此,对于每个外部API调用,我们都会返回完整的请求/响应,以及我们想要的任何内容。至少,在QA环境中。
在这里,DiagnosticData是请求/响应或我们希望返回以帮助诊断问题的任何其他内容。这是一个数组,因为我们需要允许多个东西返回。然后返回浏览器的json将始终是一个对象,它将包含一个额外的字段,并在响应中序列化diagnosticData。
希望任何做QA的人都能从浏览器中的开发工具中复制json,任何错误记录都会包括我们所做的所有API调用。通常,这就足以确定哪个团队需要采取行动。
在这种情况下,F是[DiagnosticData[],*],它的扩散正是因为我们需要向控制器提供额外的数据。
不可否认,蓝色的功能更好。getUser的最佳签名是
但这并不总是切实可行的:您通常还有其他贯穿各领域的问题:资源使用、错误处理、代码清洁度、监控。因此,如果您返回:
您为自己提供了很大的灵活性,可以不完全返回用户。我们看到了不同的例子,其中我们没有完全返回用户。您甚至可以根据上下文改变F,只返回生产之外的额外数据,或者在单元测试中是同步的,但在实际运行时是异步的。
不,不是真的。我的意思是,我认为考虑彩色函数和单子很有趣,但大多数语言不支持这些使其实用的功能。而且,用这样的编码很难让你的整个团队(包括未来的队友)参与进来。所以不要真的这么做。
我认为控制流方面主要是以相当大的概念代价让代码不那么冗长。 ↩︎
当然,javascript有异步/等待语法。我很好奇这是否等同于for expressions/do符号,并且(理论上)可以用于monad而不是Promise。 ↩︎
还有其他方法可以避免显式传递参数,但这只是为了举例。 ↩︎
它必须是单子,这样我们就可以用单子来表示。 ↩︎
支持该功能的语言称之为“高级类型”。 ↩︎