11

我很难理解supply {…}区块的用途/他们创造的按需供应。

实时供应(即来自 aSupplier并在发出值时获取新值的类型Supplier)对我来说很有意义 - 它们是异步流的一种版本,我可以使用它来将消息从一个或多个发送者广播到一个发送者或更多接收器。很容易看到响应实时消息流的用例:我可能希望每次从 GUI 界面获得 UI 事件或每次聊天应用程序广播它已收到新消息时都执行操作。

但是按需供应没有类似的意义。文档说_

点播就像 Netflix:每个开始流式传输电影(点击供应)的人,总是从头开始(获取所有值),无论现在有多少人正在观看。

好,可以。但是我为什么/什么时候需要这些语义?

这些例子也让我有点摸不着头脑。Concurancy 页面当前提供了三个块示例,但其中两个只是从循环中supply发出值。for第三个更详细一点

my $bread-supplier = Supplier.new;
my $vegetable-supplier = Supplier.new;
 
my $supply = supply {
    whenever $bread-supplier.Supply {
        emit("We've got bread: " ~ $_);
    };
    whenever $vegetable-supplier.Supply {
        emit("We've got a vegetable: " ~ $_);
    };
}
$supply.tap( -> $v { say "$v" });
 
$vegetable-supplier.emit("Radish");   # OUTPUT: «We've got a vegetable: Radish␤» 
$bread-supplier.emit("Thick sliced"); # OUTPUT: «We've got bread: Thick sliced␤» 
$vegetable-supplier.emit("Lettuce");  # OUTPUT: «We've got a vegetable: Lettuce␤» 

在那里,supply块正在做某事。具体来说,它对两个不同 (live) 的输入做出反应,Supplier然后将它们合并为一个Supply. 这似乎相当有用。

...除了如果我想转换两个Suppliers 的输出并将它们的输出合并到一个组合流中,我可以使用

my $supply = Supply.merge: 
                 $bread-supplier.Supply.map(    { "We've got bread: $_" }),
                 $vegetable-supplier.Supply.map({ "We've got a vegetable: $_" });

而且,确实,如果我用上面的/替换该supply示例中的块,我会得到完全相同的输出。此外,如果将移到对 的调用下方,块版本和/版本都不会产生任何输出,这表明块的“按需”方面在这里并没有真正发挥作用。mapmergesupplymapmergetap.emitsupply

在更一般的层面上,我不相信 Raku (或Cro)文档提供了任何块的示例,该supply块不是以某种方式转换实时输出Supply或基于for循环或发射值Supply.intervalSupply除了作为转换s的不同方式之外,这些似乎都不是特别引人注目的用例 。

鉴于上述所有情况,我很想将supply块作为一个不是那么有用的构造,除了作为某些Supply组合器的可能替代语法之外。然而,我有相当好的权威,

虽然供应商经常被联系到,但很多时候最好编写一个发出值的供应块。

鉴于此,我愿意冒险一个相当自信的猜测,即我错过了一些关于supply积木的东西。我将不胜感激任何洞察这可能是什么。

4

1 回答 1

9

鉴于您提到Supply.merge的,让我们从那开始。想象一下它不在 Raku 标准库中,我们必须实现它。为了达到正确的实施,我们需要注意什么?至少:

  1. 产生一个Supply结果,当被点击时,将......
  2. 点击(即订阅)所有输入供应。
  3. 当其中一个输入提供emitsa 值时,emit它会提供给我们的分接器...
  4. ...但请确保我们遵循串行供应规则,即我们一次只emit发送一条消息;有可能我们的两个输入源会emit同时从不同的线程中取值,所以这不是一个自动属性。
  5. 当我们所有的用品都发送了他们的done事件后,done也发送事件。
  6. 如果我们分接的任何输入电源发送quit事件,则将其中继,并关闭所有其他输入电源的分接头。
  7. 确保我们没有任何会导致破坏供应语法的奇怪比赛emit* [done|quit]
  8. 当对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是大大简化在一个或多个s上正确实现可重用操作。Supply它旨在消除的主要风险是:

  • 如果我们点击了多个 ,则无法正确处理同时到达的消息,这Supply可能会导致我们损坏状态(因为我们可能希望编写的许多供应组合器也将具有状态;merge这很简单,以至于没有)。一个supply块向我们保证,我们一次只会处理一条消息,从而消除这种危险。
  • 丢失订阅记录,从而导致资源泄漏,这将成为任何长期运行程序的问题。

第二个很容易被忽视,尤其是在使用像 Raku 这样的垃圾收集语言时。事实上,如果我开始迭代一些Seq然后在到达它的末尾之前停止这样做,迭代器将变得无法访问并且 GC 会在一段时间内吃掉它。如果我正在遍历文件的行并且那里有一个隐式文件句柄,我可能会冒文件没有及时关闭的风险,如果我不走运,可能会用完句柄,但至少有一些路径关闭并释放资源。

反应式编程不是这样:引用从生产者指向消费者,因此如果消费者“停止关心”但没有关闭水龙头,那么生产者将保留其对消费者的引用(从而导致内存泄漏)并继续发送它消息(因此做一次性工作)。这最终会导致应用程序崩溃。链接的 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直接参与直播的情况下,我们也经常会收到随时间推移来来去去的订阅。按需供应主要是关于资源的获取和释放;有时,该资源将是对 live 的订阅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-space 中的一些努力可以映射到任何东西。我个人觉得可读性较差 - 这甚至没有避免一个supply块,它只是隐藏在merge. 更棘手的情况仍然是被挖掘的供应数量随时间变化的情况,例如在递归文件监视中可能出现新目录的位置。我真的不知道如何用标准库中出现的组合子来表达这一点。

我花了一些时间教授响应式编程(不是使用 Raku,而是使用 .Net)。使用一个异步流很容易,但当我们开始处理具有多个异步流的案例时,事情就变得更加困难了。有些东西很自然地适合组合符,如“合并”或“压缩”或“组合最新”。其他人可以通过足够的创造力被撞成那种形状——但我常常觉得它是扭曲的,而不是富有表现力的。当问题不能在组合器中表达时会发生什么?在 Raku 术语中,一个人创建输出Suppliers,点击输入电源,编写将事物从输入发送到输出的逻辑,等等。每次都必须处理订阅管理、错误传播、完成传播和并发控制——而且很容易搞砸。

当然,supply方块的存在也不会停止在 Raku 中走脆弱的道路。这就是我说的意思:

虽然经常会联系到供应商,但很多时候最好编写一个发出值的供应块

我在这里并没有考虑发布/订阅的情况,我们确实想要广播值并且位于反应链的入口点。我在考虑我们挖掘一个或多个Supply,获取价值观,做某事,然后将emit事情转化为另一个的情况Supplier这是我将此类代码迁移到supply块的示例;这是稍后在同一代码库中出现的另一个示例。希望这些例子能澄清我的想法。

于 2021-10-05T23:02:30.817 回答