鉴于你提到Supply.merge
,让我们从那开始吧。想象一下,它不在 Raku 标准库中,而我们必须实现它。为了达到正确的实施,我们必须注意什么?至少:
- 制作一个
Supply
结果是,当点击时,会...
- 点击(即订阅)所有输入供应。
- 当其中一个输入电源
emit
是一个值,emit
交给我们的敲击者...
- ...但请确保我们遵循串行供应规则,即我们只
emit
一次一条消息;我们的两个输入电源可能会emit
同时从不同线程获取值,因此这不是一个自动属性。
- 当我们所有的物资都已送出时
done
事件,发送done
事件也。
- 如果我们点击的任何输入电源发送一个
quit
事件,中继它,并关闭所有其他输入电源的抽头。
- 确保我们没有任何奇怪的竞争会导致破坏供应语法
emit* [done|quit]
.
- 当点击结果时
Supply
我们生产的产品已关闭,请务必关闭我们点击的所有(仍处于活动状态)输入电源上的点击。
祝你好运!
那么标准库是如何做到的呢?像这样:
method merge(*@s) {
@s.unshift(self) if self.DEFINITE; # add if instance method
# [I elided optimizations for when there are 0 or 1 things to merge]
supply {
for @s {
whenever $_ -> \value { emit(value) }
}
}
}
要点是supply
块是大大缓解正确地实施reusable一项或多项操作Supply
是。它旨在消除的主要风险是:
- 在我们点击多个消息的情况下,无法正确处理同时到达的消息
Supply
,可能会导致我们进入腐败状态(因为我们可能希望编写的许多供给组合器也将具有状态;merge
就这么简单,不用做)。 Asupply
block 向我们承诺,我们一次只会处理一条消息,从而消除了这种危险。
- 失去订阅跟踪,从而泄漏资源,这将成为任何长时间运行的程序中的一个问题。
第二个很容易被忽视,尤其是在使用像 Raku 这样的垃圾收集语言时。事实上,如果我开始迭代一些Seq
然后在到达末尾之前停止这样做,迭代器将变得无法访问,并且 GC 会在一段时间后吃掉它。如果我正在迭代文件的行并且那里有一个隐式文件句柄,那么我将面临文件无法及时关闭的风险,并且如果我不幸的话,可能会用完句柄,但至少有some通往它的路径被关闭并释放资源。
反应式编程则不然:引用从生产者指向消费者,因此如果消费者“停止关心”但尚未关闭水龙头,那么生产者将保留其对消费者的引用(从而导致内存泄漏)并继续发送它发送消息(从而进行一次性工作)。这最终可能会导致应用程序崩溃。链接的 Cro 聊天示例就是一个示例:
my $chat = Supplier.new;
get -> 'chat' {
web-socket -> $incoming {
supply {
whenever $incoming -> $message {
$chat.emit(await $message.body-text);
}
whenever $chat -> $text {
emit $text;
}
}
}
}
当 WebSocket 客户端断开连接时会发生什么?水龙头上的Supply
我们返回使用supply
块被关闭,导致隐式close
传入 WebSocket 消息的点击次数以及$chat
。如果没有这个,订阅者列表$chat
Supplier
会无限制地增长,并反过来为每个先前的连接保持一定大小的对象图。
因此,即使在这种情况下,Supply
是非常直接的参与,我们经常会随着时间的推移而订阅它。按需供应主要是资源的获取和释放;有时,该资源将是直播的订阅Supply
.
一个公平的问题是,我们是否可以在没有supply
堵塞。是的,我们可以;这可能有效:
my $chat = Supplier.new;
get -> 'chat' {
web-socket -> $incoming {
my $emit-and-discard = $incoming.map(-> $message {
$chat.emit(await $message.body-text);
Supply.from-list()
}).flat;
Supply.merge($chat, $emit-and-discard)
}
}
注意到这是一些努力Supply
-空间映射到无。我个人认为可读性较差 - 这甚至没有避免supply
块,它只是隐藏在实现中merge
。更棘手的是,所利用的供应数量随着时间的推移而变化,例如在递归文件观看中 https://github.com/raku-community-modules/IO-Notification-Recursive/blob/master/lib/IO/Notification/Recursive.pm其中可能会出现要观看的新目录。我真的不知道如何用标准库中出现的组合器来表达这一点。
我花了一些时间教授反应式编程(不是使用 Raku,而是使用 .Net)。对于一个异步流来说事情很容易,但是当我们开始处理具有多个异步流的情况时事情变得更加困难。有些东西自然适合组合符,例如“合并”或“zip”或“组合最新”。只要有足够的创造力,其他人就可以被打造成这样的形状——但对我来说,它常常感觉扭曲而不是富有表现力。当问题无法用组合器表达时会发生什么?用 Raku 术语来说,一个人创造输出Supplier
s,利用输入电源,编写将输入内容发送到输出的逻辑,等等。每次都必须处理订阅管理、错误传播、完成传播和并发控制——而且很容易搞砸。
当然,存在supply
Blocks 也并没有停止在 Raku 中走上脆弱的道路。这就是我说的意思:
虽然经常会找到供应商,但很多时候最好编写一个发出值的供应块
我在这里没有考虑发布/订阅的情况,我们确实想要广播值并且处于反应链的入口点。我正在考虑我们点击一个或多个的情况Supply
,采取价值观,做某事,然后emit
事物变成另一种事物Supplier
. 这是一个例子 https://github.com/shuppet/raku-api-discord/commit/ad31a677e99f936e3ac56a43744ffeb55bf062e6我将这样的代码迁移到supply
block; 这是另一个例子 https://github.com/shuppet/raku-api-discord/commit/363102830ecef0fa93a82d2490277a9bd1375af1稍后在同一个代码库中出现。希望这些例子能澄清我的想法。