如何重构代码以使其具有函数式风格?

How to refactor code to make it functional style?

在玩 F# 的过程中,我试图以一种更实用的方式来思考代码。我的大部分工作恰好是数字性质的,所以我在想这种再教育是否有意义。以函数式方式编写数字代码,就像试图在圆孔中安装方钉一样,还是不管应用程序如何,这只是一个陡峭的学习曲线问题?

例如,让我们看一段演示弱大数定律的片段:

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

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

我觉得这是一个很好的问题。我的印象是,在编写功能性数字代码(想想 Matlab 与 Mathematica)时,您 运行 遇到的麻烦不是语法,而是性能。但同时也很容易将代码并行化。

我会这样写你的代码:

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

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

我还会研究是否可以直接使用 Exponential.Sample 生成样本,而不是调用它 1,000 次。

编辑

像这样:Exponential.Samples(0.1) |> 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)

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

我首先不会将对 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.th 元素的总和这一事实在 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

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

与性能有关吗?

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

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

如果性能有点问题,您可能希望在不可变性上做出妥协。 F# 中函数代码的主要成本来自垃圾收集器,尤其是来自具有中间生命周期的对象。使昂贵的对象可变并重新使用它们可以在执行速度上产生巨大的差异。如果您想以简洁和安全的方式编写流体动力学、n 体模拟或游戏等内容,但不希望达到快速的执行速度,那么多范式 F# 风格可能是一种很好的方式去吧。

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

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