在F#中功能性地写入平方和

时间:2016-04-21 14:57:19

标签: f# functional-programming

所以我有这4个例子,其中3个来自youtube video

我刚刚参加了函数式编程课程(在Racket中),如果我对F#的基本理解是对的,我就是这样。

let data = [1.;2.;3.;4.]

let sqr x = x * x

// Bad, very bad   
let sumOfSquareI nums =
    let mutable acc = 0.0
    for x in nums do
        acc <- acc + sqr x
    acc

// Better than above, but may cause stack overflow with very long list    
let rec sumOfSquareF1 nums = 
    match nums with 
    | [] -> 0.0
    | h::t -> sqr h + sumOfSquareF1 t

// much much better, uses tail-recursion, no stack overflow  
let sumOfSquareF2 nums =
    let rec sumOfSquareLocal nums acc = 
        match nums with
        | [] -> acc
        | h::t -> sumOfSquareLocal t (acc + sqr h)
    sumOfSquareLocal nums 0.0

// seems to be idiomatic F#, but is it better than tail-recursive version?    
let sumOfSquare nums =
    nums
    |> Seq.map sqr
    |> Seq.sum    

sumOfSquare data
sumOfSquareI data
sumOfSquareF1 data
sumOfSquareF2 data

最后两个函数之间有什么真正的区别吗?这个比那个好吗?在F#中编写功能代码时,我应该多久使用一次|&gt;操作员(这对我来说是全新的)?

另一件事是在F#中创建列表有不同的方法吗?我从视频中得到的东西似乎......笨重。

5 个答案:

答案 0 :(得分:6)

是的,你应该更喜欢使用SeqList模块中的高阶函数而不是显式递归。您也可以使用sumBy

let sumOfSquare nums = nums |> Seq.sumBy sqr

答案 1 :(得分:4)

关于编程风格或范例的讨论很容易变成基于意见的推理。让我们来看看每个函数在实际影响中真正重要的是什么。

第1版:势在必行

这个花费五条琐碎的行。几乎每个人都可以阅读它。 &#34;状态机&#34;是一个单一的变量,易于验证。它不是通用的,但是LanguagePrimitives.GenericZero代替0.0可以解决这个问题,虽然代价是一定的。

我不打电话给这个&#34;糟糕,非常糟糕&#34;与其他人相比。除非你把编程作为一种崇拜,否则必然会被神圣的功能真理所摧毁。尽管如此,它并不是最小的。

版本2:递归计算

它相当可读,使用较少的一行。同样,不是通用的,但通用零可以解决这个问题。 然而,此功能将导致堆栈增长!有人使用它会在第一次测试中看到它正常工作,直到突然,性能直线下降或整个程序崩溃!

我认为这是客观上最差的解决方案!您不希望在生产代码中使用此功能!

版本3:嵌套尾递归函数

同样,这可以是通用的。这个很好,功能很好,但它有6行,而且我不再称其中三个是微不足道的。

这是纯粹的,但老实说,如果我必须在这个和命令式变体之间做出选择,我不太确定。

版本4:调用高阶函数

简短易读,通用而不使用奇怪的原语!你为什么怀疑这是四个人的赢家?

额外版本:完全没有

高阶函数对此非常有效,但这不是结束。 Seq.sumBy sqr(我看到Lee在他的回答中打败了我)完成了这项工作。由于这仅比sumOfSquares长,因此在生产代码中,根本没有理由定义额外的功能。

可读性是关键

当您询问有关何时使用管道运营商等的建议时,答案必须针对将要阅读代码的人进行优化。编译器肯定不关心你抛出多少个括号。想一想谁将阅读代码,并为此进行优化。

在为自己编写代码时,使用任何组合都会花费您最少的时间来导航和解释。管道非常棒,可以避免像很多关闭的括号一样难以计算。

PS:有很多方法可以初始化lists。在这种情况下,[1. .. 4.]可以胜任。

答案 2 :(得分:2)

您的功能之间最重要的区别在于它们的通用性。我们来看看他们的类型:

val sumOfSquareI : nums:seq<float> -> float
val sumOfSquareF1 : nums:float list -> float
val sumOfSquareF2 : nums:float list -> float
val sumOfSquare : nums:seq<float> -> float

正如您所看到的,sumOfSquaresumOfSquareI比其他更通用 - 它们适用于任何序列,而不仅仅是列表。

sumOfSquaresumOfSquareF2的风格差异而言,我个人更喜欢sumOfSquare。由于管道操作员和没有递归,你可以简单地从上到下阅读代码,在每一行你都知道发生了什么。

正如李指出的那样,你可以使用内置的Seq.sumBy函数:

let sumOfSquareBiref nums = nums |> Seq.sumBy sqr

如果稍后在文件中调用此函数(以便可以推断输入参数),您甚至可以跳过参数并写入:

let sumOfSquareBirefer = Seq.sumBy sqr

此时,您必须问自己是否要定义单独的函数,或者只是在代码中内联(Seq.sumBy sqr input)。您应该考虑该功能是否可能发生变化,以及它在源中的使用次数。

答案 3 :(得分:2)

正如@Lee指出的那样,高阶函数通常比F#中的显式递归更受欢迎。

为了向您添加另一个解决方案,对于F#中的数字算法,将函数标记为inline通常更为惯用,例如

let inline sumOfSquares s = Seq.sumBy (fun x -> pown x 2) s

因此它的签名变为:

val sumOfSquares : s:seq<'a> -> 'a (requires member get_Zero and member (+) and member get_One and member (*) and member (/))

这意味着您可以在任何原始数字类型上调用它:

sumOfSquares <| seq { for x in 1..5 -> x } // seq<int>
sumOfSqaures <| seq { for x in 1. .. 5. -> x } // seq<float>
sumOfSqaures [1..5] // int list
sumOfSquares [1. .. 5.] // float list

更通用且可重复使用。

答案 4 :(得分:1)

您可以使用列表推导创建列表,或者只使用[1..10]

创建列表