【问题标题】:compilation issue with split/splitter拆分/拆分器的编译问题
【发布时间】:2015-10-28 00:03:55
【问题描述】:

下面是简单的代码:

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,它将编译。

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

【问题讨论】:

    标签: d phobos


    【解决方案1】:

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

    以下是该库背后的原因以及您也可以用来实现它的其他一些想法:

    这不能编译的原因与 std.algorithm 和 range 背后的理念有关:它们尽可能便宜地将成本决策推向顶层。

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

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

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

    无论如何,readText 返回一个具有大量功能的范围,包括splitter 需要的可保存性。为什么splitter 需要保存?考虑它承诺的结果:从最后一个分割点开始并继续到下一个分割点的一系列字符串。为最通用的范围编写此代码时,实现看起来像什么?

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

    所以,由于splitter 的实现需要save 起点的能力,它至少需要一个向前的范围(这只是一个可保存的范围。安德烈现在觉得这样命名有点愚蠢,因为有这么多名字,但在他写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)....也许这就是原因就在那里:您将迭代项目两次,一次在算法内部,然后再次在算法外部,并且库认为提高一个级别的成本足够高。(谓词版本通过制作 your 函数来回避这一点进行扫描,因此 Phobos 认为它​​不再是它的问题,你知道你自己的函数在做什么。)

    我可以看到其中的逻辑。不过我可以双向选择,因为实际上再次碾压它的决定仍然在外面,但我不明白为什么不经过深思熟虑就不可取。

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

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

    filter 改变了这一点。过滤掉 [1,2,3] 的奇数只得到 [2]:长度不同,现在在开头而不是中间找到 2。但是,在您实际应用过滤器之前,您无法知道它在哪里 - 如果不缓冲结果,您将无法四处跳动。

    splitter 类似于过滤器。它改变了元素的位置,并且算法不知道它在哪里分裂,直到它真正穿过元素。所以它可以在你迭代时告诉你,但不是在迭代之前,所以索引将是 O(n) 速度 - 计算上太昂贵了。索引应该非常便宜。


    无论如何,既然我们了解了原理的存在 - 让您,最终程序员可以就缓冲(这需要比可用内存更多的内存)或额外的迭代(这需要比成本更多的 CPU 时间)等昂贵的事情做出决定-free to the algorithm),并通过考虑其实现来了解为什么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 更喜欢程序员,他了解您的程序的需求,如果您是否愿意支付这些费用,做出决定,而不是在不告诉您的情况下代表您支付。

    【讨论】:

    • 很好的答案。谢谢。
    猜你喜欢
    • 1970-01-01
    • 2016-06-19
    • 1970-01-01
    • 2011-06-13
    • 2010-11-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-04
    相关资源
    最近更新 更多