从内部更改 `whenever` 块的目标
Posted
技术标签:
【中文标题】从内部更改 `whenever` 块的目标【英文标题】:Changing the target of a `whenever` block from the inside 【发布时间】:2021-11-27 04:40:40 【问题描述】:以下代码尝试对一个Supply
做出反应,然后根据某些消息的内容改变主意并对来自不同Supply
的消息做出反应。这是一种尝试提供与 Supply.migrate 类似的行为,但具有更多控制权。
my $c1 = Supplier.new;
my $c2 = Supplier.new;
my $s = supply
my $currently-listening-to = $c1.Supply;
my $other-var = 'foo';
whenever $currently-listening-to
say "got: $_";
if .starts-with('3')
say "listening to something new";
$currently-listening-to = $c2.Supply;
$other-var = 'bar';
say $other-var;
$s.tap;
for ^7
$c1.emit: "$_ from \$c1";
$c2.emit: "$_ from \$c2";
sleep 10;
如果我正确理解了supply
块的语义(非常值得怀疑!),则该块应该对supply
块内声明的任何变量具有独占且可变的访问权限。因此,我希望这会从 $c1
获取前 4 个值,然后切换到 $c2
。然而,事实并非如此。这是输出:
ot: 0 from $c1
got: 1 from $c1
got: 2 from $c1
got: 3 from $c1
listening to something new
bar
got: 4 from $c1
got: 5 from $c1
got: 6 from $c1
正如该输出所示,更改 $other-var
的工作与我预期的一样,但更改 $currently-listening-to
的尝试失败(静默)。
这种行为正确吗?如果是这样,我对 supply
块/其他解释这种行为的构造的语义缺少什么?我使用 react
块和使用 Channel
而不是 Supply
时得到了相同的结果,因此在多个并发构造中行为是一致的。
(为了避免 XY 问题,触发此问题的用例是尝试实现 Erlang 样式的错误处理。为此,我希望有一个监督 supply
块来监听其子级和可以杀死/重新启动任何进入不良状态的孩子。但这意味着要听新的孩子——这直接导致了上述问题。)
【问题讨论】:
【参考方案1】:我倾向于将whenever
视为for
的响应式等效项。 (它甚至支持LAST
循环移相器在Supply
为done
时执行某些操作,以及支持next
、last
和redo
就像普通的for
循环一样!)考虑这个:
my $x = (1,2,3);
for $x<>
.say;
$x = (4,5,6);
输出是:
1
2
3
因为在for
循环的设置阶段,我们获得了一个迭代器,然后处理它,而不是在每次迭代时再次读取$x
。与whenever
相同:它点击Supply
,然后根据emit
事件调用主体。
因此需要另一个whenever
来实现对下一个Supply
的点击,同时关闭当前的点击。当只考虑两个Supply
s 时,写它的简单方法是这样的:
my $c1 = Supplier.new;
my $c2 = Supplier.new;
my $s = supply
whenever $c1
say "got: $_";
if .starts-with('3')
say "listening to something new";
# Tap the next Supply...
whenever $c2
say "got: $_";
# ...and close the tap on the current one.
last;
$s.tap;
for ^7
$c1.emit: "$_ from \$c1";
$c2.emit: "$_ from \$c2";
这将产生:
got: 0 from $c1
got: 1 from $c1
got: 2 from $c1
got: 3 from $c1
listening to something new
got: 3 from $c2
got: 4 from $c2
got: 5 from $c2
got: 6 from $c2
(请注意,我删除了sleep 10
,因为不需要它;在这个例子中我们没有引入任何并发,所以一切都是同步运行的。)
显然,如果有十几个 Supply
s 可以在它们之间移动,那么这种方法的扩展性就不会那么好。那么migrate
是如何工作的呢?关键缺失的部分是我们可以在使用whenever
时获得Tap
句柄,因此我们可以从whenever
的主体外部关闭它。这正是migrate
的工作原理(从标准库复制,添加了 cmets):
method migrate(Supply:D:)
supply
# The Tap of the Supply we are currently emitting values from
my $current;
# Tap the Supply of Supply that we'll migrate between
whenever self -> \inner
# Make sure we produce a sensible error
X::Supply::Migrate::Needs.new.throw
unless inner ~~ Supply;
# Close the tap on whatever we are currently tapping
$current.close if $current;
# Tap the new thing and store the Tap handle
$current = do whenever inner -> \value
emit(value);
简而言之:您不要更改whenever
的目标,而是启动一个新的whenever
并终止前一个。
【讨论】:
“我倾向于将whenever
视为for
的反应式等价物”。谢谢,这非常有帮助——我认为supply
/whenever
是@987654356 的反应式等价物@/when
,所以我期待能够更改whenever
的“条件”,就像我可以更改when
的“条件”一样。但我现在看到whenever
实际上是在做(反应性)“迭代”,而不仅仅是像when
那样匹配迭代的结果
确实如此。可迭代空间中的supply
等价于gather
。例如,一个人可以做gather for @outers -> @inners for @inners -> $value take $value;
,产生一个可以从多个其他人迭代的结果。以上是关于从内部更改 `whenever` 块的目标的主要内容,如果未能解决你的问题,请参考以下文章