【问题标题】:Recursive block expansion error when recursively yielding递归产生时递归块扩展错误
【发布时间】:2018-03-22 15:55:36
【问题描述】:

我希望在 Crystal 中实现 Python 的 os.walk 方法。我试图递归地执行它,但编译器被告知要小心递归产生,因为它会递归/无限地在编译时生成代码。这就是我所拥有的

def walk(d = @root, &block)
  d = Dir.new(d) if d.is_a?(String)
  dirs, files = d.entries.partition { |s| Dir.exists?(File.join(d.path, s)) }
  if Dir.exists?(d.path)
    yield d.path, dirs, files
    dirs.each do |dir_name|
      # recursively yield
      walk File.join(d.path, dir_name), do |a, b, c|
        yield a, b, c
      end
    end
  end
end

【问题讨论】:

    标签: recursion crystal-lang


    【解决方案1】:

    Gitter 社区的一些乐于助人的成员为我指明了正确的方向,只是想在这里分享我的经验。答案是,你不能递归地使用yield,但你必须使用block 变量来代替(后面会有解释)。这是我最终得到的结果:

    def walk(d = @root, &block : String, Array(String), Array(String) -> )
      d = Dir.new(d) if d.is_a?(String)
      dirs, files = d.children.partition { |s| Dir.exists?(File.join(d.path, s)) }
      block.call(d.path, dirs, files)
      dirs.each do |dir_name|
        walk File.join(d.path, dir_name), &block
      end
    end
    

    这里的诀窍是,您必须使用block.call 代替yield 关键字并转发您的块。这实际上是文档中的already,但有点微妙。在编译过程中,如果你有一个yield,编译器会字面上将你的块内联到 yield 所在的位置(据我所知)。当使用block.call 时,会创建一个函数,这就是我们需要输入块参数的原因。如果你不给它一个类型,block.call 将期望 0 个参数。要通过,只需键入类似于我在此方法签名中的操作方式。

    根据上面的解释,当你只屈服并且它就可以工作时,为什么你不需要向block 添加类型是有道理的。了解yieldblock.call 之间存在性能差异的原因也很重要,因为在一种情况下会创建关闭函数,而编译器会内联您的代码。

    【讨论】:

    • 使用Dir#each_child 而不是Dir#entries,您不需要过滤掉...。此外,在为该路径中的条目创建迭代器之后调用 Dir.exists? 也是不合逻辑的。
    • 再看一眼,Dir#childrenDir#entries 的替代品 - 谢谢!至于Dir#exists,我不是在检查是否存在,而是路径是一个目录。如果有更好的方法,请告诉我,但是查看 Dir#exists 的代码,除了 File.stat 的包装器并检查它是否是目录之外,它看起来并没有做太多的事情。不知道如何以更便宜的方式检查路径是否是目录
    • 你不需要检查d.path是否存在。如果没有,Dir.new 已经提出。
    • 哦,我明白你现在的意思了(正在查看错误的行)。好点,谢谢。刚刚更新