【问题标题】:How to refactor code to make it functional style?如何重构代码以使其具有功能性?
【发布时间】:2016-07-07 05:04:47
【问题描述】:

在使用 F# 时,我试图以更实用的方式来思考代码。我的大部分工作恰好是数字性质的,所以我在想这种再教育是否有意义。以一种实用的方式编写数字代码,就像试图将一个方形钉子安装在一个圆孔中,还是仅仅是一个陡峭的学习曲线问题,而与应用程序无关?

例如,让我们看一个演示大数弱定律的 sn-p:

open System
open System.IO
open System.Windows.Forms
open System.Windows.Forms.DataVisualization
open FSharp.Data
open FSharp.Charting
open FSharp.Core.Operators
open MathNet.Numerics
open MathNet.Numerics.LinearAlgebra
open MathNet.Numerics.Random
open MathNet.Numerics.Distributions
open MathNet.Numerics.Statistics


let T = 1000

let arr1 = Array.init T (fun i -> float i*0.)
for i in 0 .. T-1 do
    arr1.[i] <- [|for j in 1..i do yield Exponential.Sample(0.1)|] |> Statistics.Mean

let arr2 = Array.init T (fun i -> float i*0.)
for i in 0 .. T-1 do
    arr2.[i] <- arr1.[1 .. i] |> Statistics.Mean

arr2 |> Chart.Line |> Chart.Show

是否有一种简洁的功能方式来表达上述内容?有多少功能范式可以融入这样的工作中?

【问题讨论】:

  • 顺便说一句,F# for Scientists 一书虽然有点过时了。还有Real World Functional Programming的摘录。也许最近的书籍对数学网有更好的解释。

标签: performance f# math.net mathnet-numerics


【解决方案1】:

这实际上是两个问题:一个是关于改进给定代码,另一个是关于 F# 中的功能性数字代码。由于其他答案已经关注特定代码,因此我将关注更一般的问题。

是关于性能吗?

根据我的经验,数值函数式编程的适用性取决于性能要求。执行时间越重要,您可能越想在功能样式上妥协。

如果性能不是问题,函数式代码往往运行良好。它简洁且安全,比命令式编程更接近数学写作。当然,有些问题可以很好地映射到命令式程序,但总的来说,函数式风格是一个很好的默认选择。

如果性能有点问题,您可能需要在不变性上做出妥协。 F# 中函数式代码的主要成本来自垃圾收集器,尤其是来自具有中间生命周期的对象。使昂贵的对象可变并重新使用它们可以大大提高执行速度。如果您想以简洁且安全的方式编写诸如流体动力学、n 体模拟或游戏之类的东西,但不以踏板到金属的执行速度为目标,那么多范式 F# 风格可能是一个很好的方式去吧。

如果性能就是一切,那么很可能,您无论如何都需要 GPU 执行。或者可以充分利用 CPU 向量单元、多线程等。虽然有人尝试在 GPU 上使用 F#,但该语言并非不惜一切代价为速度而设计。在这种情况下,使用更接近硬件的语言可能会更好。

当问题是这些问题的混合时,通常可以混合解决方案。例如,昨天我需要对一组图像进行逐像素计算,执行时​​间很重要。所以我使用 .NET 库读取 F# 中的图像,然后将它们与转换像素的 GLSL 计算着色器一起上传到 GPU,然后将它们下载回“F#land”。这里的重点是管理操作效率不高;代码仍在无缘无故地复制东西。但它只是一个操作会吃掉所有的性能,所以使用高性能工具来完成一个操作是合理的,而其余的一切都在 F# 中整齐而安全地发生。

【讨论】:

  • 很高兴有人选择详细讨论这部分问题。我还要补充一点,您可以走不同的路线来提高性能。当涉及到小的常数因子加速时,命令式代码总是会获胜,但是如何使用更抽象的函数式代码设计具有更好算法复杂性的算法通常更容易和更明显。出于这个原因,我认为对于已经解决的问题,使用经过验证的命令式解决方案通常会更好,但对于新颖的开发,更多功能代码可能会更好地服务。
【解决方案2】:

我首先不会分开对Array.init 的调用并设置初始值。您可以使用@s952163 在他们的回答中使用的表格,或者根据您的代码:

let arr1 = Array.init T (fun i ->
    [|for j in 1..i do yield Exponential.Sample 0.1 |] |> Statistics.Mean
)

这个问题是您分配了中间数组,这很昂贵 - 而且您无论如何都会在计算平均值后立即丢弃它们。替代方案:

let arr1 = Array.init T (fun i ->
    Exponential.Samples 0.1 |> Seq.take (i+1) |> Seq.average
)

现在是第二部分:您正在重复计算元素 1..i 的平均值,这变成了 O(n^2) 操作。您可以通过使用元素 1..i 的总和是元素 1..{i-1} 加上第 i 个元素的和这一事实在 O(n) 中解决它。

let sums, _ =
    arr1
    |> Array.mapFold (fun sumSoFar xi ->
        let s = sumSoFar + xi
        s, s
    ) 0.0
let arr2 = 
    sums
    |> Array.mapi (fun i sumi -> sumi / (float (i + 1)))

当然,你们都可以在一个管道中编写。

或者,使用库函数Array.scan 来计算累积和,在这种情况下,这将为您提供长度为T+1 的结果,然后您将从中删除第一个元素:

let arr2 =
    Array.sub (Array.scan (+) 0.0 arr1) 1 T
    |> Array.mapi (fun i sumi -> sumi / (float (i + 1)))

或避免中间数组:

Seq.scan (+) 0.0 arr1
|> Seq.skip 1
|> Seq.mapi (fun i sumi -> sumi / (float (i + 1)))
|> Seq.toArray

【讨论】:

  • 是的!我正在等待折叠的东西:)
【解决方案3】:

我认为这是一个非常好的问题。我的印象是,在编写函数式数字代码(想想 Matlab 与 Mathematica)时遇到的麻烦不在于语法,而在于性能。但同时也很容易并行化代码。

我会这样写你的代码:

let arr1' = [|for i in 0..1000 -&gt; Array.init i (fun i -&gt; Exponential.Sample(0.1)) |&gt; Statistics.Mean |]

请注意,a) 没有可变赋值,b) 没有索引,c) 我没有初始化基于 0 的数组并填充它,而是使用函数初始化数组。

我还会调查是否可以直接使用 Exponential.Sample 生成样本,而不是调用 1000 次。

编辑

像这样:Exponential.Samples(0.1) |&gt; Seq.take 1000

根据@ChristophRüegg 的以下评论:

let expoMean (x:float []) =
    Exponential.Samples(x,0.1)
    x |> Statistics.Mean

Array.init 1000 (fun _ -> Array.replicate 1000 0. |> expoMean)

我没有对此进行基准测试。

【讨论】:

  • 最快的方式,内部并行化,就是创建一个数组,然后传给静态的Exponential.Samples(array, 0.1)去填充。
猜你喜欢
  • 1970-01-01
  • 2019-03-01
  • 2018-11-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-07-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多