使用F#获取目录树表示的更好方法?

时间:2014-12-01 07:32:07

标签: f# tail-recursion

我是F#的新手(ish),我正在尝试获取文件系统目录的树形图。这就是我想出的:

type FSEntry =
  | File of name:string
  | Directory of name:string * entries:seq<FSEntry>

let BuildFSDirectoryTreeNonTailRecursive path = 
  let rec GetEntries (directoryInfo:System.IO.DirectoryInfo) =
    directoryInfo.EnumerateFileSystemInfos("*", System.IO.SearchOption.TopDirectoryOnly)
    |> Seq.map (fun info -> 
        match info with 
        | :? System.IO.FileInfo as file -> File (file.Name)
        | :? System.IO.DirectoryInfo as dir -> Directory (dir.Name, GetEntries dir)
        | _ -> failwith "Illegal FileSystemInfo type"
        )

  let directoryInfo = System.IO.DirectoryInfo path
  Directory (path, GetEntries directoryInfo)

但是......很确定这不是尾递归。我看了一下生成的IL并没有看到任何尾部前缀。有一个更好的方法吗?我尝试使用累加器,但没有看到它有多大帮助。我尝试了相互递归函数,无处可去。也许继续可行,但我发现这令人困惑。

(我知道在这种特殊情况下,堆栈深度不会是的问题但是仍然想知道如何解决这种非尾递归问题)

OTOH,似乎确实奏效了。以下打印出我期待的内容:

let PrintFSEntry fsEntry =
  let rec printFSEntryHelper indent entry  =
      match entry with
      | File name -> printfn "%s%s" indent name
      | Directory(name, entries) ->
        printfn "%s\\%s" indent name
        entries 
        |> Seq.sortBy (function | File name -> 0 | Directory (name, entries) -> 1)
        |> Seq.iter (printFSEntryHelper (indent + "  "))

  printFSEntryHelper "" fsEntry

这应该是一个不同的问题,但是......如何测试BuildFSDirectoryTreeNonTailRecursive?我想我可以像在C#中那样创建一个界面并模拟它,但我认为F#有更好的方法。

已编辑:根据初始评论,我指定我知道堆栈空间可能不是问题。我还指出我主要关注测试第一个函数。

1 个答案:

答案 0 :(得分:1)

要扩展我之前的评论 - 除非你预期使用会导致堆栈溢出而没有尾递归的输入,否则从函数尾递归中无法获得任何东西。对于您的情况,限制因素是路径名中的~260个字符,超过该字符,大多数Windows API将开始中断。在由于非尾递归而开始耗尽堆栈空间之前,你会遇到这种情况。

对于测试,您希望您的功能尽可能接近纯函数。这涉及重构副作用的功能部分。这两种函数都是这种情况 - 其中一种隐式依赖于文件系统,另一种直接将文本打印到标准输出。

我想我所建议的重构与Mark Seemann的观点相当接近:几个模拟 - 检查,接口很少 - 检查,功能组合 - 检查。然而,你所拥有的例子并没有很好地适应它,因为它是EnumerateFileSystemInfo之上极薄的贴面。我可以像这样摆脱System.IO:

type FSInfo = DirInfo of string * string | FileInfo of string

let build enumerate path = 
    let rec entries path = 
        enumerate path
        |> Seq.map (fun info ->
           match info with
           | DirInfo (name, path) ->  Directory(name, entries path)
           | FileInfo name -> File name)

    Directory(path, entries path)

现在我留下了一个enumerate: string -> seq<FSInfo>函数,可以轻松替换为甚至不触及驱动器的测试实现。然后枚举的默认实现是:

let enumerateFileSystem path = 
    let directoryInfo = DirectoryInfo(path)
    directoryInfo.EnumerateFileSystemInfos("*", System.IO.SearchOption.TopDirectoryOnly)
    |> Seq.map (fun info -> 
        match info with 
        | :? System.IO.FileInfo as file -> FileInfo (file.Name)
        | :? System.IO.DirectoryInfo as dir -> DirInfo (dir.Name, dir.FullName)
        | _ -> failwith "Illegal FileSystemInfo type")

你可以看到它与build函数具有几乎相同的形状,减去递归,因为逻辑的整个“核心”都在EnumerateFileSystemInfos之内,它超出了你的代码。这是一个小小的改进,不是以任何方式测试引起的损坏,但它仍然不会很快成为任何人的幻灯片。