所以我有这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#中创建列表有不同的方法吗?我从视频中得到的东西似乎......笨重。
答案 0 :(得分:6)
是的,你应该更喜欢使用Seq
和List
模块中的高阶函数而不是显式递归。您也可以使用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
正如您所看到的,sumOfSquare
和sumOfSquareI
比其他更通用 - 它们适用于任何序列,而不仅仅是列表。
就sumOfSquare
和sumOfSquareF2
的风格差异而言,我个人更喜欢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]