【问题标题】:Idiomatic F# - Simple Statistical Functions惯用 F# - 简单的统计函数
【发布时间】:2019-03-20 01:32:41
【问题描述】:

作为一个简单挑战的一部分,我将从头开始编写几个简单的统计函数,并尝试以最“惯用的 F#”方式编写它们。我是函数式编程的新手,所以我想学习如何从一开始就创建简单的东西。

这是我目前所拥有的:

let mean (x : float list) : float =
    (List.sum x) / (float (List.length x))

let variance (x : float list) : float =
    x
    |> List.map (fun a -> pown (a - (mean x)) 2)
    |> mean

let stdDev =
    variance >> Math.Sqrt

我喜欢 stdDev 函数是如何使用组合定义的,但我觉得可能有一些更漂亮、更惯用的方式来定义前两个。

有什么建议吗?

【问题讨论】:

    标签: f# idioms


    【解决方案1】:

    您的代码非常完美且惯用。

    就我个人而言,我更喜欢尽可能使用一种衬垫。这样我就可以对齐代码以突出功能之间的异同。模式就是这样跳到你身上。

    let mean     x = (Seq.sum x) / (float (Seq.length x))
    let variance x = let m = mean x
                     x |> Seq.map (fun a -> pown (a - m) 2) |> mean
    let stdDev   x = x |> variance |> Math.Sqrt
    

    我也更喜欢seq 而不是list,因为它们可以与列表、数组、集合或任何其他序列一起使用。

    do  [| 5. ; 6. ; 7. |] |> stdDev |> printfn "%A"
    do  [  5. ; 6. ; 7.  ] |> stdDev |> printfn "%A"    
    Set [  5. ; 6. ; 7.  ] |> stdDev |> printfn "%A"
    seq [  5. ; 6. ; 7.  ] |> stdDev |> printfn "%A"        
    seq {  5.   ..   7.  } |> stdDev |> printfn "%A"        
    

    在 F# 中最好避免使用 >> 组合运算符,而改用管道 |>。 编写这样的函数有很多问题。例如上面的代码是不可能的(使用不同的类型,如列表和数组)。

    【讨论】:

      【解决方案2】:

      函数组合算子并不像它的名声那么糟糕,它只是需要小心一点,不要碰到value restriction,或相关问题之一。同名错误 FS0030 说:

      要么明确“stdDev”的参数,要么,如果你不 想要它是通用的,添加一个类型注释。

      我们还可以添加类型注释,以使 let-bound 值比其他方式推断的更通用。

      let mean : seq<_> -> _ = 
          Seq.fold (fun (s, l) t -> s + t, l + 1) (0., 0) >> function
          | _, 0 -> failwith "empty collection"
          | s, l -> s / float l
      let variance x = x |> Seq.map (x |> mean |> (-) >> fun a -> a * a) |> mean
      let stdDev : seq<_> -> _ = variance >> sqrt
      [5. ; 6. ; 7.] |> stdDev |> printfn "%A"    // prints 0.8164965809
      {5.   ..   7.} |> stdDev |> printfn "%A"    // prints 0.8164965809
      

      如果没有注解,meanstdDev 都不会在这里编译而不会因为值限制而在模块中被调用。即使这样,它们也仅限于遇到的第一种实现 System.Collections.Generic.IEnumerable&lt;'T&gt; 的类型。

      另一方面,variance 的定义不适合 eta 归约,它不会遇到这些问题。组合用于组合两个函数:从partially applied 均值中减去,然后将值乘以自身。

      【讨论】:

      • 感谢您的回答!这里有很多有用的东西。如果mean 必须始终采用float(我相信是因为s / float l 操作),那么保持让绑定值更通用有什么意义?此外,在组合函数上,我们实际上是在执行(mean(X) - x)^2,对吧?我知道这个例子并不重要,但是操作数的顺序是我在管道方面遇到的问题。确保(x - mean(X)) 的正确方法是x |&gt; Seq.map (x |&gt; mean |&gt; (~-) |&gt; (+)) 之类的吗?这看起来很混乱......
      • 好的。别介意第一个问题。我现在意识到您的意思是SeqList`Array` 意义上的更通用的类型,没有它,使用该函数的第一个结构实际上将定义该函数必须接受的类型。在这种情况下,使用seq&lt;_&gt; -&gt; _seq&lt;float&gt; -&gt; float 有什么区别吗?
      • @tls 诚然,variance 的映射写法不应该被认为是惯用的;它可能更好地表达为((-) (mean x) &gt;&gt; fun a -&gt; a * a)。如果您知道类型并且不希望它是通用的,您也可以指定它;只是seq&lt;float&gt; -&gt; float 需要输入更多内容。
      【解决方案3】:

      可能值得做的一个小改动是从 variance 函数中的 lambda 函数主体中提取 mean x 调用。 F# 编译器可能不会自动为您执行此操作,因此您最终会重新计算列表中每个元素的平均值:

      let variance (x : float list) : float =
          let mx = mean x
          x
          |> List.map (fun a -> pown (a - mx) 2)
          |> mean
      

      正如 AMieres 在另一个回复中提到的,您还可以考虑使用与列表不同的类型。 List 很好用而且很实用,但是Seq 将使代码适用于任何集合。或者,如果您使用更大的数据进行计算,Array 可能会更快一些。

      【讨论】:

      • 好电话。我刚刚在[ 1.0 .. 100000.0 ] 上运行了两个版本并得到:Real: 00:06:29.198, CPU: 00:00:43.546, GC gen0: 1, gen1: 0, gen2: 0Real: 00:00:00.050, CPU: 00:00:00.031, GC gen0: 1, gen1: 1, gen2: 0。您对选择类型有何建议?将Seq 用于“通用”代码,将ListArray 用于更具体的用例?在处理惰性求值序列时,我只使用过Seq...
      猜你喜欢
      • 1970-01-01
      • 2022-12-05
      • 1970-01-01
      • 2021-01-05
      • 2010-10-18
      • 2019-04-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多