Comparing F# with C#: Sorting

https://swlaschin.gitbooks.io/fsharpforfunandprofit/content/posts/fvsc-quicksort.html

在下一个示例中,我们将实现类似快速排序的算法来对列表进行排序,并将 F# 实现与 C# 实现进行比较。

以下是简化的类似快速排序算法的逻辑:

如果列表为空,则无事可做。
否则:
1. 取列表的第一个元素
2. 在列表的其余部分找到所有小于第一个元素的元素,并对它们进行排序。
3. 在列表的其余部分中找到所有大于等于第一个元素的元素,并对它们进行排序
4. 将三部分组合在一起得到最终结果:
(已排序的较小元素 + firstElement + 排序较大的元素)

请注意,这是一个简化的算法,没有经过优化(而且它没有像真正的quicksort那样进行原地排序);我们现在要关注的是语法的清晰度。

这是 F# 中的代码:

let rec quicksort list =
   match list with
   | [] ->                            // If the list is empty
        []                            // return an empty list
   | firstElem::otherElements ->      // If the list is not empty     
        let smallerElements =         // extract the smaller ones    
            otherElements             
            |> List.filter (fun e -> e < firstElem) 
            |> quicksort              // and sort them
        let largerElements =          // extract the large ones
            otherElements 
            |> List.filter (fun e -> e >= firstElem)
            |> quicksort              // and sort them
        // Combine the 3 parts into a new list and return it
        List.concat [smallerElements; [firstElem]; largerElements]

//test
printfn "%A" (quicksort [1;5;23;18;9;1;3])

再次注意,这不是优化的实现,而是旨在说明算法在语法上的密度。

让我们看一下这段代码:

  • 任何地方都没有类型声明。此函数适用于任何具有可比较项的列表(几乎所有 F# 类型,因为它们自动具有默认比较函数)。

  • 整个函数是递归的——这是使用“let rec quicksort list =”中的 rec 关键字向编译器发出信号。

  • match..with 有点像 switch/case 语句。每个要测试的分支都用竖线表示,如下所示:

  • [] “模式” 会“匹配”一个空列表,并返回一个空列表。

  • firstElem::otherElements 的“模式匹配”做了两件事。

    • 首先,它只匹配一个非空列表。

    • 第二,它自动创建两个新的值。一个用于第一个元素,称为 "firstElem",另一个用于列表的其他部分,称为 "otherElements"。用C#的话说,这就像有一个 "switch"语句,它不仅是分支,而且同时进行变量声明和赋值。

  • -> 有点像 C# 中的 lambda (=>)。等效的 C# lambda 看起来像 (firstElem, otherElements) => 函数体。

  • smallerElements”部分获取列表的其余部分,使用带有“<”运算符的内联 lambda 表达式针对第一个元素以外的其他元素,使用第一个元素进行过滤,然后将结果递归地传递到快速排序函数中。

  • largerElements”行做同样的事情,除了使用“>=”运算符

  • 最后,使用列表连接函数“List.concat”构造结果列表。为此,需要将第一个元素放入列表中,这就是方括号的作用。

  • 再次注意没有“return”关键字;最后一个值将被返回。在“[]”分支中,返回值为空列表,在主分支中,返回值为新构造的列表。【也就是这一切都是表达式的组合,没有显式return,中间看到的赋值啥的,也只是某个表达式的表达而已,实际上如果用lazy的方式来看,到实际发起求值以前,一切都只是符号系统的积累,只有到实际求值的时候才会出现真正的计算,当然,由于CLR本身式严格求值的,所以这个说法本身在F#里是不成立的,但是这么思考会有助于理解什么是表达式的组合这个概念

为了进行比较,这里有一个旧式 C# 实现(不使用 LINQ)。

比较两组代码,我们再次可以看到 F# 代码更紧凑,噪音更小,并且不需要类型声明。

此外,与 C# 代码不同,F# 代码读起来几乎与实际算法完全一样。这是 F# 的另一个关键优势——与 C# 相比,代码通常更具声明性(“做什么”)和更少命令性(“如何做”),因此更加自文档化。

C# 中的函数式实现

下面是使用 LINQ 和扩展方法的更现代的“函数式”实现:

这更简洁,并且与 F# 版本几乎相同。但不幸的是,没有办法避免函数签名中的额外噪音。

Correctness 正确性

最后,这种紧凑性的一个有益的副作用是 F# 代码通常一次就能运行,而 C# 代码可能需要更多调试。

事实上,在编写这些示例时,旧式 C# 代码最初是不正确的,需要进行一些调试才能使其正确。特别棘手的地方是 for 循环(从 1 而不是零开始)和 CompareTo 比较(我弄错了方法),而且很容易意外修改原始列表。第二个 C# 示例中的函数式风格不仅更简洁,而且更容易正确编码。

但与 F# 版本相比,即使是函数式 C# 版本也有缺点。例如,因为 F# 使用模式匹配,所以不可能分支到空列表的“非空列表”情况。另一方面,在 C# 代码中,如果我们忘记了测试:

然后提取第一个元素:

会失败并出现异常。编译器无法为您强制执行此操作。在您自己的代码中,如果你经常使用 FirstOrDefault 而不是 First ,那是因为因为你经常在编写“防御性”代码。下面是一个在 C# 中很常见但在 F# 中很少见的代码模式示例:

F# 中的一步“模式匹配和分支”使您在许多情况下可以避免这种情况。

Postscript 后记

按照 F# 标准,上面 F# 中的示例实现实际上非常冗长!

为了好玩,这是一个更典型的简洁版本:

4行代码还挺好理解的,而且当你习惯了语法之后,还是很好读的。

Last updated