在函数式反应式编程中缓存流

Caching streams in Functional Reactive Programming

我有一个完全使用 FRP 范例编写的应用程序,我认为由于我创建流的方式,我遇到了性能问题。它是用 Haxe 编写的,但问题不是特定于语言的。

例如,我有这个函数,它 returns 每次为特定部分更新配置文件时解析的流,如下所示:

function getConfigSection(section:String) : Stream<Map<String, String>> {
    return configFileUpdated()
        .then(filterForSectionChanged(section))
        .then(readFile)
        .then(parseYaml);
}

在我使用的响应式编程库中,称为 promhx 链的每个步骤都应记住其最后解析的值,但我认为每次调用此函数时我都在重新创建流并重新处理每个步骤。这是我使用它的方式而不是库的问题。

由于此函数在任何地方都被调用,因此每次需要时解析 YAML 都会降低性能,并且根据分析占用了超过 CPU 时间的 50%。

作为修复,我使用存储为缓存流的实例变量的 Map 完成了如下操作:

function getConfigSection(section:String) : Stream<Map<String, String>> {
    var cachedStream = this._streamCache.get(section);
    if (cachedStream != null) {
        return cachedStream;
    }

    var stream = configFileUpdated()
        .filter(sectionFilter(section))
        .then(readFile)
        .then(parseYaml);

    this._streamCache.set(section, stream);
    return stream;
}

这可能是解决问题的好方法,但我觉得不合适。我想知道是否有人能想到一个更简洁的解决方案,它可能使用更实用的方法(闭包等),甚至是我可以像缓存函数一样添加到流中的扩展。

我可以做到的另一种方法是预先创建流并将它们存储在消费者可以访问的字段中。我不喜欢这种方法,因为我不想为每个配置部分创建一个字段,我喜欢能够调用具有特定部分的函数并返回流。

我喜欢任何可以给我全新视角的想法!

好吧,我认为一个答案就是抽象掉缓存 like so:

class Test {
    static function main() {
        var sideeffects = 0;
        var cached = memoize(function (x) return x + sideeffects++);
        cached(1);
        trace(sideeffects);//1
        cached(1);
        trace(sideeffects);//1
        cached(3);
        trace(sideeffects);//2
        cached(3);
        trace(sideeffects);//2
    }
    @:generic static function memoize<In, Out>(f:In->Out):In->Out {
        var m = new Map<In, Out>();
        return
            function (input:In) 
                return switch m[input] {
                    case null: m[input] = f(input);
                    case output: output;
                }
    }
}

您以后可能会找到 memoize 的更多 "functional" 实现。但重要的是,它现在是一个单独的东西,你可以随意使用它。

您可以选择 memoize(parseYaml),这样在文件中切换两个状态实际上会在两个状态都被解析一次后变得非常便宜。您还可以调整 memoize 以根据证明最有价值的任何策略来管理缓存大小。