从内部更改 `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 循环移相器在Supplydone 时执行某些操作,以及支持nextlastredo 就像普通的for 循环一样!)考虑这个:

my $x = (1,2,3);
for $x<> 
    .say;
    $x = (4,5,6);

输出是:

1
2
3

因为在for 循环的设置阶段,我们获得了一个迭代器,然后处理它,而不是在每次迭代时再次读取$x。与whenever 相同:它点击Supply,然后根据emit 事件调用主体。

因此需要另一个whenever 来实现对下一个Supply 的点击,同时关闭当前的点击。当只考虑两个Supplys 时,写它的简单方法是这样的:

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,因为不需要它;在这个例子中我们没有引入任何并发,所以一切都是同步运行的。)

显然,如果有十几个 Supplys 可以在它们之间移动,那么这种方法的扩展性就不会那么好。那么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 -&gt; @inners for @inners -&gt; $value take $value; ,产生一个可以从多个其他人迭代的结果。

以上是关于从内部更改 `whenever` 块的目标的主要内容,如果未能解决你的问题,请参考以下文章

更改特定目标 Fastlane 的版本和内部版本号

基本块的组成部分是啥? (例如分支、目标、入口、出口)

如何从目标 C 更改 CBAdvertisementDataLocalNameKey 值?

js如何给目标元素的兄弟元素更改样式

:hover 更改其他元素,但它也从目标触发[重复]

如何随时检测内部的Rails环境