erlang 从内部更改"wherefore“块的目标

2ekbmq32  于 2022-12-08  发布在  Erlang
关注(0)|答案(1)|浏览(105)

下面的代码尝试对一个Supply做出React,然后根据一些消息的内容改变主意,对来自另一个Supply的消息做出React。它试图提供与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时得到了相同的结果,因此这种行为在多个并发结构中是一致的。
(In为了避免X-Y问题,触发这个问题的用例是尝试实现Erlang风格的错误处理。为此,我希望有一个监督supply块,它监听其子块,并可以杀死/重新启动任何进入坏状态的子块。但这意味着监听新的子块-这直接导致了上述问题。)

balp4ylt

balp4ylt1#

I tend to consider whenever as the reactive equivalent of for . (It even supports the LAST loop phaser for doing something when the tapped Supply is done , as well as supporting next , last , and redo like an ordinary for loop!) Consider this:

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

The output is:

1
2
3

Because at the setup stage of a for loop, we obtain an iterator, and then work through that, not reading $x again on each iteration. It's the same with whenever : it taps the Supply and then the body is invoked per emit event.
Thus another whenever is needed to achieve a tap of the next Supply , while simultaneously closing the tap on the current one. When there are just two Supply s under consideration, the easy way to write it is like this:

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";
}

Which will produce:

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

(Note that I removed the sleep 10 because there's no need for it; we aren't introducing any concurrency in this example, so everything runs synchronously.)
Clearly, if there were a dozen Supply s to move between then this approach won't scale so well. So how does migrate work? The key missing piece is that we can obtain the Tap handle when working with whenever , and thus we are able to close it from outside of the body of that whenever . This is exactly how migrate works (copied from the standard library, with comments added):

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);
            }
        }
    }
}

In short: you don't change the target of the whenever , but rather start a new whenever and terminate the previous one.

相关问题