递归 C# 函数从 for 循环内部返回 - 如何转换为 F#?

Posted

技术标签:

【中文标题】递归 C# 函数从 for 循环内部返回 - 如何转换为 F#?【英文标题】:Recursive C# function returns from inside a for-loop - how to translate to F#? 【发布时间】:2017-05-09 08:45:15 【问题描述】:

长期 C# 开发人员,学习 F#。我选择了一段相当函数式的 C# 代码,作为学习练习编写的 - 将其转换为 F#。我已经阅读了大量有关函数式编程的内容,并经常使用 C# 中的函数式构造,但我学习 F# 的时间只有几个小时。

此功能是解决类似于“LonPos 101”的谜题的程序的一部分,您可以在亚马逊等网站上找到该谜题。求解器中使用的策略是基于认识到谜题空间中只有 30 个有效位置,因此“到目前为止的解决方案”可以用一个整数表示,并且每个部分的每个有效位置也可以用一个整数表示,并且完整的解决方案是一组包含 7 个部分中的每一个中的一个可能位置的集合,其中7 件的“重量”加起来就是解决方案的重量 (2^30-1)。在给定的函数中,“key”是片段的“主键”,wbk 是“keys by key” - 由键索引,包含相应片段的有效位置列表,而“w”是前面提到的“解决方案” -迄今为止”。返回值是从键到所选位置的映射,并在成功递归的退出路径上填充,从而导致解决方案。我在开发 C# 解决方案时发现,将其设为排序列表会使解决方案查找器的速度提高一个数量级,但对于普通列表同样有效。

这是我遇到问题的 C# 函数:

int solutionWeight;

Dictionary<int,int> Evaluate(int w, Dictionary<int, SortedSet<int>> wbk, int key)

  if (w == solutionWeight)
    return new Dictionary<int, int>();  

  if (key == 8)
    return null;

  foreach (var w2 in wbk[key])
  
    if ((w & w2) != 0)
      continue;
    var s = Evaluate(w | w2, wbk, key + 1);
    if (s != null)
    
      s.Add(key, w2);
      return s;
    
  

  return null;

这是我对 F# 版本的尝试 - 它可以编译,但无法正常工作 - 在执行 w 不是 solutionWeight 和 key 的情况时,它最终在 let ss=... 行中出现 KeyNotFoundException 失败等于 8。在这种情况下为什么要执行这行代码对我来说毫无意义,但是...

    let rec Evaluate(w:int, wbk:Dictionary<int, SortedSet<int>>, key:int):Dictionary<int,int> =
    if w = solutionWeight then 
        Dictionary<int,int>()
    else if key = 8 then 
        null
    else
        // ... this is wrong - runs off the end of some collection - fails with key not found exception
        let ws = wbk.[key] |> Seq.filter (fun w2 -> (w2 &&& w) = 0) 
        /// ... for some reason, execution resumes here after the key = 8 clause above
        let ss = ws |> Seq.map (fun w -> (w,Evaluate(w, wbk, key+1))) 
        let sw = ss |> Seq.find (fun sw -> snd sw <> null) 
        let s = snd sw 
        s.Add(key, fst sw)
        s

为我指明正确的方向!我的下一个尝试是首先以更实用的风格重新编写 C# 代码,但感觉这个版本即将开始工作(虽然可能离惯用的 F# 还很远)。

更新:

我重写了 F# 函数,通过使用一对相互递归的函数来消除循环。它确实有效,但它比非互递归 C# 版本慢约 2 倍。

let rec Evaluate(w:int, wbk:Dictionary<int, SortedSet<int>>, key:int):Dictionary<int,int> =
    if w = solutionWeight then 
        Dictionary<int,int>()
    else if key = 8 then 
        null
    else
        EvalHelper(w, wbk, key, wbk.[key].GetEnumerator())

and EvalHelper(w:int, wbk:Dictionary<int, SortedSet<int>>, key:int, ws:IEnumerator<int>):Dictionary<int,int> =
    if ws.MoveNext() then
        let w2 = ws.Current
        if (w &&& w2) = 0 then
            let s = Evaluate(w ||| w2, wbk, key+ 1)
            if s <> null then
                s.Add(key, w2)
                s
            else
                EvalHelper(w, wbk, key, ws)
        else
            EvalHelper(w, wbk, key, ws)
    else
        null

我该如何进一步改进它?

更新:我对其进行了更多调整 - 感觉有点 F#ish,但我仍然觉得我应该能够摆脱更多类型注释并更多地使用 F# 原生类型。这是一项正在进行的工作。

let rec Evaluate(w, wbk:Dictionary<int, SortedSet<int>>, key):Dictionary<int,int> option =
    let rec EvalHelper(ws) =
        match ws with
        | w2 :: mws ->
            match w &&& w2 with
            | 0 ->
                let s = Evaluate(w ||| w2, wbk, key+ 1)
                match s with
                | None -> EvalHelper(mws)
                | Some s ->
                    s.Add(key, w2)
                    Some(s)
            | _ -> EvalHelper(mws)
        | _ ->
            None

    if w = solutionWeight then 
        Some (Dictionary<int,int>())
    else if key = 8 then 
        None
    else
        EvalHelper(List.ofSeq wbk.[key])

【问题讨论】:

使用函数式编码时,您应该首先考虑最简单的表达式,然后将它们组合成更复杂的表达式,直到您拥有所需的内容。换句话说,从下往上看,而不是自上而下。将您的功能分解成越来越小的部分,确保它们正常工作,然后将它们组合起来。 这绝对是我的下一个策略——这个程序总体上大量使用了不可变数据结构和函数构造——但这个特殊的函数——并没有那么多。 How can I further improve it? 看看Option Type 而不是使用null。使用for 而不是MoveNext。使用match 而不是使用嵌套的if then。将guards 与match 一起使用。如果可能,请使用 nested function 而不是 let rec ... and ... 我会记住这些,谢谢!是时候为 F# 找一个新项目了——这个就在罐子里。 如果您喜欢自己的答案,请将其发布为答案。如果看不到答案,人们往往会跳过看问题。帮自己一个忙,获得一些额外的分数,特别是到 50,这样你就可以comment everywhere。您可以在发布后随时编辑您的答案。 【参考方案1】:

翻译此函数的关键是将 for 循环转换为递归,如第一次更新所示。

let rec Evaluate(w:int, wbk:Dictionary<int, SortedSet<int>>, key:int):Dictionary<int,int> =
    if w = solutionWeight then 
        Dictionary<int,int>()
    else if key = 8 then 
        null
    else
        EvalHelper(w, wbk, key, wbk.[key].GetEnumerator())

and EvalHelper(w:int, wbk:Dictionary<int, SortedSet<int>>, key:int, ws:IEnumerator<int>):Dictionary<int,int> =
    if ws.MoveNext() then
        let w2 = ws.Current
        if (w &&& w2) = 0 then
            let s = Evaluate(w ||| w2, wbk, key+ 1)
            if s <> null then
                s.Add(key, w2)
                s
            else
                EvalHelper(w, wbk, key, ws)
        else
            EvalHelper(w, wbk, key, ws)
    else
        null

随后的更新只是稍微清理了样式 - 使其更接近惯用的 F#。

let rec Evaluate(w, wbk:Dictionary<int, SortedSet<int>>, key):Dictionary<int,int> option =
    let rec EvalHelper(ws) =
        match ws with
        | w2 :: mws ->
            match w &&& w2 with
            | 0 ->
                let s = Evaluate(w ||| w2, wbk, key+ 1)
                match s with
                | None -> EvalHelper(mws)
                | Some s ->
                    s.Add(key, w2)
                    Some(s)
            | _ -> EvalHelper(mws)
        | _ ->
            None

    if w = solutionWeight then 
        Some (Dictionary<int,int>())
    else if key = 8 then 
        None
    else
        EvalHelper(List.ofSeq wbk.[key])

【讨论】:

你有我4年。我确实使用打孔卡而不是纸带。您是否使用了纸带和/或拨动开关?如果链接/参考被破坏,答案也应该是自包含的。虽然您的答案显然在问题中,但程序员是我认识的最懒惰的人,但这是一件好事。 我打孔卡(Fortran 67 - 是的!)但没有纸带。在许多支持磁带的系统上工作,但从未走这条路。美好时光。感谢您的提示 - 不用担心编辑我的答案。你说程序员是我认识的最懒惰的人是绝对正确的! @GuyCoder 哦,这一切都具有讽刺意味。使用 .NET 反编译器(到 C#)查看已编译的程序集,我可以看到编译器实际上采用了我的递归 EvalHelper 函数并将其转换回生成代码中的循环。查看生成的 IL,这个程序的 F# 版本比 C# 版本慢大约 1.5 倍也就不足为奇了 - 令人印象深刻的是它并不比这更差。 当我编写函数式代码时,我首先想到的不是它有多快,而是它有多准确。你知道algebraic data types 了吗? 是的,我至少了解代数数据类型。您在此处链接的有趣系列-我完成了它的一部分,将在新的一年里完成它:) 我尝试(非常努力!)避免过早的优化-但作为一个刚开始使用

以上是关于递归 C# 函数从 for 循环内部返回 - 如何转换为 F#?的主要内容,如果未能解决你的问题,请参考以下文章

当for内部有查询时如何返回异步函数的值

从递归函数expressjs返回空数组?

递归函数中的for循环在递归结束后继续

使用递归方法和for循环方法求阶乘

迭代器协议和for循环工作机制

如何在 Python 中没有任何循环(例如:for、while 等)递归 JSON 文件