为 Akka 演员的世界带来类型安全是多年来一直在讨论和研究的事情。这些持续努力的当前表现是Akka Typed API,它可能会发生变化。
除了链接的文档之外,几年前关于 Akka 用户列表的精彩讨论(标题为“如何协调无类型的 Actor 与有类型的编程?”)提供了对类型化 Actor 的进一步了解。在此处阅读整个讨论以了解整个上下文,但以下是一些摘录:
从德里克怀亚特:
你正在经历的是一种权衡。演员提供了一种你似乎没有考虑到的权衡;端点(Actor)是无类型的,它们处理的消息是强类型的。
您不能让 Actor 能够使用特定于类型的接收方法来处理“任何事情”。使用 Actor 编程,我应该能够在消息流中添加任意数量的中介,而不会干扰两个端点。中介同样应该不知道正在发生的事情(负载平衡器、路由器、记录器、缓存器、中介、分散收集等)。您还应该能够在不干扰端点的情况下在集群中移动它们。您还可以在 Actor 中设置动态委托,而无需真正理解正在发生的事情 - 例如,说“主要方言”但不理解所说内容时委托给其他内容的 Actor。
如果您想消除所有这些功能,那么您将能够获得您正在寻找的类型安全确定性(只要您保持在同一个 JVM 中 - 跨 JVM 将导致“我到底是什么说话?”消除编译时间保证的问题)....
简而言之,您正在放弃类型安全性,以便打开一整套全新设施的大门。不想失去类型安全?关门 :)
从恩德雷瓦尔加:
问题是类型系统是为本地计算而不是分布式计算而设计的。让我们看一个例子。
想象一个具有 A、B 和 C 三种状态的演员
- 在状态 A 中,它接受类型 X 的消息,当收到一个时,它转换到 B
- 在状态 B 它接受 X 和 Y 类型的消息。当收到 X 时,转换到 C,如果是 Y,则停留在 B
- 在状态 C 中,它接受类型为 Z 的消息
现在,您从状态 A 开始向参与者发送消息 X。可能会发生两件事:
- X 已交付,因此可能接受的类型是 {X, Y}
- X 丢失了,所以接受的类型是 {X}
它们的交集是{X}。
现在假设您发送另一条消息 X。可能会发生三件事:
- 两个 X 都已交付,因此接受的类型是 {Z}
- 只有一个 X 被交付,另一个丢失,所以接受的类型是 {X, Y}
- 两个 X 都丢失了,接受的类型是 {X}
上述情况的交集是空集。
那么你发送了两条 X 类型消息的 Actor 的本地类型表示应该是什么?
让我们修改示例,假设没有消息丢失,但让我们从另一个发送者的角度来看。这个发件人知道另一个发件人向我们的示例演员发送了两个 X。我们可以发送什么消息?有以下三种情况:
- 另一个发送者发送的两个 X 都已经到达,所以接受的类型是 {Z}
- 只有另一个发送者发送的第一个 X 已经到达,所以接受的类型是 {X, Y}
- 还没有 X,接受的类型是 {X}
上述情况的交集是空集。
如您所见,在没有收到参与者回复的情况下,参与者的可证明类型通常是 Nothing 或无用的东西。只有回复才能传达可能的参与者类型,如果有并发发送者,即使这样也无法保证。
来自Roland Kuhn 博士:
我很高兴你提出这个讨论,我希望为 Akka 添加某种程度的静态类型与我参与该项目一样古老。如果您查看过去的 1.x,您会发现 akka.actor.Channel[T] 是考虑到这一点而构思的,并且在 2.1 和 2.2 中有 Typed Channels 作为基于宏的实验。后者实际上跨越了从思想实验到代码的界限,欢迎您尝试一下,以了解静态类型如何与非常动态的系统交互。
Typed Channels 的主要缺点是其不适当的复杂性(类型参数太多,类型太复杂——其中包含类型级别的列表和映射)。我们正在逐渐趋同于一个可能达到适当平衡的设计,但从本质上讲,这意味着sender
从 Akka 演员中移除(这还有其他非常受欢迎的好处,即在 Future 转换中关闭事物)。它的要点是使用它接受的消息类型来参数化 ActorRef[T](对 Props[T]、Actor[T] 等具有明显的连锁效应)。然后,Actor 可以使用适当的类型公开对自身的引用,并将其发送给其他 Actor——在特定消息中,以绕过类型擦除。这甚至允许制定消息协议,即会话类型或至少接近它。
Derek 提出了一个很好的观点,即 Actor 模型如何真正受益于不受类型的约束:消息路由器不一定需要知道有关通过它的消息的任何信息。参数化路由器本身的效果如何还有待观察,但通常这样的路由阶段会破坏类型信息,我们无能为力。你的观点是,有一些类型检查总比没有好,这与我产生了很好的共鸣,只要对开发人员来说差异真的很明显:我们必须避免错误的安全感。
这让我想到了 Endre 的有效感叹,即静态验证无法访问并发行为。这个问题比消息丢失要广泛得多,因为任何不确定的动作都必须导致类型析取,通过类型结构的指数爆炸杀死我们好的静态类型。这意味着我们实际上只能使用类型来表达那些确定性的部分:如果您向参与者发送 A 类型的消息,那么您可能会收到 B 类型的消息(这意味着必须提供 ActorRef[B]在 A 消息中),其中 A 和 B 通常是总和类型,例如“此参与者接受的所有命令”和“可能发送的所有回复”。不可能对参与者的定性状态变化进行建模,因为编译器无法知道它们是否会实际发生。
不过有一些亮点:如果您收到消息 B,其中包括来自目标的 ActorRef[C],那么您有证据表明消息 A 的效果已经发生,因此您可以假设演员现在处于以下状态它接受消息 C。但这不是保证,actor 可能在此期间崩溃了。
请注意,这些都不依赖于远程消息传递。您将参与者拆分为并发和分发部分的愿望非常容易理解,我曾经也这么认为。后来才明白并发和分布式其实是一回事:进程只有在空间或时间上分开执行才能并发运行,也就是说是分布式的,而光速有限又意味着分布式进程根据定义,将是并发的。我们想要对我们的actor进行封装和划分,只使用消息进行通信,而这种模型意味着两个actor总是相互分离的,即使它们运行在同一个JVM上它们也是分布式的(队列可以运行满,可能会发生故障,通信并不完全可靠——尽管它的可靠性肯定比网络案例高很多)。如果您考虑现代处理器,不同的内核,尤其是套接字也由网络分隔,它们比您祖父的千兆以太网快得多。
这正是为什么我相信 Actor 模型是对现在和将来应用程序中的独立部分进行建模的正确抽象,因为硬件本身越来越分布式,而 Actor 恰好捕捉到了其中的本质。正如我在上面所说的,我确实看到了静态类型方面的改进空间。