split/splitter 的编译问题

compilation issue with split/splitter

这是简单的代码:

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

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

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

底线:通常可以通过在有问题的函数之前调用 .array 来解决此类问题。这为它提供了一个具有足够功能的缓冲区 运行 算法。

接下来是库背后的推理以及您也可以用来实现它的其他一些想法:

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

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

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

所以您的代码会发生以下情况:readText returns 一个数组,这是一个几乎全功能的范围。 (由于它 return 是一个 string,由 UTF-8 组成,就 Phobos 而言,它实际上并不提供随机访问(尽管令人困惑,语言本身对它的看法不同,搜索 D "autodecode" 争论的论坛,如果你想了解更多)因为在可变长度的 utf-8 字符列表中找到一个 Unicode 代码点需要扫描它。扫描它不是最小的成本,所以 Phobos 永远不会除非您特别要求,否则请尝试。)

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

大致如下:首先,save 您的起始位置,以便稍后可以 return。然后,使用 popFront 前进,直到找到分割点。当它这样做时,return 保存的范围到分割点的点。然后,popFront 越过分割点并重复该过程,直到您吃完整个东西 (while(!input.empty))。

所以,由于splitter的实现需要save起点的能力,它至少需要一个前向范围(这只是一个可保存的范围。安德烈现在觉得命名像这有点傻,因为名字太多了,但在他写作的时候std.algorithm他仍然相信给他们所有的名字)。

并非所有射程都是向前射程!数组是,保存它们就像从当前位置 returning 切片一样简单。许多数值算法也是如此,保存它们只是意味着保留当前状态的副本。大多数变换范围都是可保存的,如果它们正在变换的范围是可保存的——同样,他们需要做的就是 return 当前状态。

......在我写这篇文章时,实际上,我认为你的例子 应该 是可以保存的。而且,确实,有一个重载需要一个谓词并编译!

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 拒绝一般切片......但知道它实际上可以无论如何,所以该函数是特殊情况. 你在整个图书馆都看到了。)

但是,为什么它需要切片而不是仅仅保存?老实说,我不知道。也许我也遗漏了一些东西,但确实有效的重载的存在对我来说意味着我对算法的概念是正确的; 可以这样做。我确实相信切片更便宜一些,但保存版本也足够便宜(你会计算你弹出了多少项目才能到达拆分器,然后 return saved.take(that_count).. .. 也许这就是原因所在:您将对项目进行两次迭代,一次在算法内部,然后再次在算法外部,并且库认为它的成本足以提升一个级别。(谓词版本通过使 您的 函数进行扫描,因此 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 更喜欢您的程序员,他了解您的程序的需求并且这些费用你愿意不愿意支付,自己做决定,而不是它不告诉你就代你支付。