拆分/拆分器的编译问题

时间:2015-10-28 00:03:55

标签: d phobos

这是一个简单的代码:

import std.algorithm;
import std.array;
import std.file;

void main(string[] args)
{
    auto t = args[1].readText()
        .splitter('\n')
        .split("---")
    ;
}

看起来它应该可以工作,但它不会编译。 DMD 2.068.2因此错误而失败:

Error: template std.algorithm.iteration.splitter cannot deduce function from
argument types !()(Result, string), candidates are:
...
Error: template instance std.array.split!(Result, string) error instantiating

如果我在.array之前插入.split,则会进行编译。

我错过了什么吗?或者这是一个错误?我试图在错误跟踪器中进行简短搜索,但没有找到任何内容。

1 个答案:

答案 0 :(得分:19)

底线:这样的问题通常可以通过在违规功能之前粘贴.array来解决。这为缓冲区提供了足够的功能来运行算法。

以下是图书馆背后的原因以及您可以用来实现这一点的其他一些想法:

这不编译的原因与std.algorithm和范围背后的哲学有关:它们尽可能便宜,以便将成本决策推向顶层。

在std.algorithm(以及大多数编写良好的范围和范围消耗算法)中,模板约束将拒绝任何不提供免费所需的输入。同样,转换范围(如过滤器,分离器等)将仅返回它们能够以最低成本提供的功能。

通过在编译时拒绝它们,它们迫使程序员在最高级别决定他们如何支付这些费用。您可能会重写该函数以使其工作方式不同,您可以使用各种技术自行缓冲它以预先支付成本,或者您可以找到其他任何有效的工具。

所以,您的代码会发生什么:readText返回一个数组,这是一个几乎功能齐全的范围。 (因为它返回一个由UTF-8组成的string,就Phobos而言,它实际上并不提供随机访问(但令人困惑的是,语言本身看到的方式不同,搜索D论坛如果你想了解更多,那么" autodecode"争议呢?因为在可变长度的utf-8字符列表中找到一个Unicode代码点需要扫描所有内容。扫描所有内容并不是最低成本,因此Phobos将会除非你特别要求,否则不要尝试。)

无论如何,readText会返回一个包含大量功能的范围,包括splitter需要的可保存性。为什么splitter需要保存?考虑它承诺的结果:从最后一个分割点开始并继续到下一个分割点的一系列字符串。在为最普通的范围编写它时,实现是什么样的,它可能做得便宜?

沿着这些方向:首先,save您的起始位置,以便您稍后可以返回。然后,使用popFront,前进直到找到分割点。如果是这样,请将保存的范围返回到分割点。然后,popFront超过分割点并重复此过程,直到您消耗了整个事物(while(!input.empty))。

所以,由于splitter的实现需要save起点的能力,它至少需要一个前进范围(这只是一个可以保存的范围.Andrei现在感觉命名的东西这样有点傻,因为有这么多名字,但在他写作std.algorithm时,他仍然相信给他们所有的名字。)

并非所有范围都是前进范围!数组是保存它们就像从当前位置返回切片一样简单。许多数值算法也是如此,保存它们只意味着保留当前状态的副本。如果他们正在变换的范围是可以保存的,那么大多数变换范围都是可以保存的 - 再次,他们需要做的就是返回当前状态。

......正如我写的那样,实际上,我认为你的示例应该可以保存。事实上,有一个带有谓词和编译的重载!

http://dlang.org/phobos/std_algorithm_iteration.html#.splitter.3

    import std.algorithm;
    import std.array;
    import std.stdio;

    void main(string[] args)
    {
            auto t = "foo\n---\nbar"
                    .splitter('\n')
                    .filter!(e => e.length)
                    .splitter!(a => a == "---")
            ;
            writeln(t);
    }

输出:[["foo"], ["bar"]]

是的,它在与特定事物相等的行上编译和分割。另一个重载.splitter("---")无法编译,因为该重载需要切片功能(或一个狭窄的字符串,Phobos拒绝一般切片......但实际上它知道它可以是,所以该函数是特殊的你在整个图书馆都看到了。)

但是,为什么需要切片而不仅仅是保存?老实说,我不知道。也许我也错过了一些东西,但是有效的重载的存在意味着我对算法的概念是正确的; 可以以这种方式完成。我确实认为切片有点便宜,但保存版本也足够便宜(你要记住你弹出过多少件物品才能到达分离器,然后返回saved.take(that_count) ....也许这就是原因:你会在算法内部迭代两次,然后再在外面,并且库认为填充一个级别的成本足够高。(谓词版本通过制作< em>你的函数进行扫描,因此Phobos认为它不再是它的问题,你知道你自己的函数正在做什么。)

我可以看到其中的逻辑。我可以双管齐下,但是,实际上再次尝试重新开始的决定仍然在外面,但我不明白为什么在没有一些想法的情况下这可能是不可取的。

最后,为什么splitter没有为其输出提供索引或切片?为什么没有filter提供它?为什么map提供它?

嗯,它再次与低成本理念有关。 map可以提供它(假设它的输入有),因为map实际上并没有改变元素的数量:输出中的第一个元素也是输入中的第一个元素,只是一些函数在结果上运行。最后也是如此,其他所有人都是。

filter改变了这一点。过滤出奇数[1,2,3]只得[2]:长度不同,现在在开头而不是中间找到2。但是,在实际应用过滤器之前,您无法知道 的位置 - 您无法在不缓冲结果的情况下跳转。

splitter与过滤器类似。它改变了元素的位置,算法并不知道 它分裂的位置,直到它实际穿过元素。所以它可以告诉你迭代,但不能在迭代之前,因此索引速度O(n) - 计算成本太高。索引应该是非常便宜的。

无论如何,现在我们理解为什么会有这样的原则 - 让你,最终程序员做出关于缓冲(需要更多内存而不是免费)或额外迭代(需要更多CPU时间而非成本)等昂贵事情的决策 - 对算法没有任何想法,并且通过考虑它的实现来了解为什么splitter需要它,我们可以看看满足算法的方法:我们需要使用占用更多CPU的版本循环并使用我们的自定义比较函数编写它(参见上面的示例),或以某种方式提供切片。最直接的方法是在数组中缓冲结果。

import std.algorithm;
import std.array;
import std.file;

void main(string[] args)
{
    auto t = args[1].readText()
        .splitter('\n')
        .array // add an explicit buffering call, understanding this will cost us some memory and cpu time
        .split("---")
    ;
}

您也可以在本地缓冲它或者自己来减少分配成本,但无论如何,成本必须在某处支付,而Phobos更喜欢程序员,他们了解程序的需求,如果你是否愿意支付这些费用,做出决定而不是代表你付钱而不告诉你。