PureScript - 计算数字数组的平均值

PureScript - Calculate the mean of an array of numbers

考虑以下 PureScript 代码

-- returns the mean value of the numbers in an array
mean :: Array Number -> Number
mean numbers = 
  -- code goes here

如何编写这个函数?

JavaScript 中的等价物如下所示:

function mean (numbers) {
  let g = 0;
  for (let n of numbers) {
    g += n;
  }
  return g / numbers.length;
}

但我不知道如何在 mean 函数中创建局部变量 g 或者是否有一些不同的、更面向 PureScript 的方法。

如何在 PureScript 中找到一组数字的平均值?

虽然从技术上讲,您可以在 PureScript 中使用变异,但它既笨拙又不舒服,这是故意的:PureScript 故意让它变得困难,以阻止您使用它。突变是错误的来源。现在是现代 goto。尽可能避免。

而是使用函数转换。

例如,平均值是所有数字的总和除以它们的数量。所以就写下来吧:

mean numbers = sum numbers / toNumber (length numbers)

(当然这对于空数组会失败,让我们暂时忽略这个事实)


或者您实际上是在问如何计算总和?或者,更狭义地说,如何在 JavaScript 示例中执行与 for 循环等效的操作?

迭代的最终基础是递归。每一步的递归函数检查是否需要继续迭代。如果有,它会再次调用自己,如果没有,它不会。

Singly-linked 列表(存在于所有 FP 语言中)迭代起来很愉快,因为它们的结构本身反映了递归计算。不幸的是,在 PureScript 中我们必须(默认情况下)处理数组。数组没有令人愉快的递归结构,但您仍然可以使用索引作为迭代媒介进行迭代:

sum :: Array Number -> Int -> Number
sum arr startAt = 
  case arr !! startAt of
    Just x -> x + sum arr (startAt + 1)
    Nothing -> 0

在这里,在每次迭代中,我都在检查当前索引是否仍在数组中,同时获取该索引处的元素。如果是,说明我需要继续迭代,所以我递归调用自己。如果不是,说明我完了,我不叫自己了

当然,这里的一个不便之处是此类函数的消费者必须传递一个额外的参数startAt。这可以通过将实际的递归函数隐藏在外观后面来很好地处理:

sum :: Array Number -> Number
sum arr = go 0
  where
    go startAt = 
      case arr !! startAt of
        Just x -> x + go (startAt + 1)
        Nothing -> 0

使用此类功能需要注意的一件事是它不是 stack-safe。它必须执行与数组中的元素一样多的嵌套调用,所以如果太多,它会炸毁堆栈。

为了解决这个问题,任何递归算法都可以变成所谓的尾递归。这个术语意味着以这样一种方式编写递归函数,即每次它调用自身时,它不会对 return 值做任何事情,除了直接 return 之外。这样写就可以在不消耗栈的情况下执行递归了。

对于上面的示例,将其转换为尾递归的方法是传递“到目前为止的总和”以及当前索引:

sum :: Array Number -> Number
sum arr = go 0 0.0
  where
    go startIndex sumSoFar =
      case arr !! startIndex of
        Just x -> go (startIndex + 1) (sumSoFar + x)
        Nothing -> sumSoFar

但实际上显式递归并不经常使用。我不会说“从不”甚至“很少”,但这不是 go-to。相反,我们通常使用其他更简单的函数,这些函数本身是建立在递归基础上的。

一个示例位于此答案的最顶部 - 来自 Data.Foldablesum。但是最通用的函数是 fold(有两种变体 - foldr and foldl

,它抓住了迭代本身的本质

使用foldlsum函数可以这样写:

sum :: Array Number -> Number
sum arr = foldl (\x sumSoFar -> x + sumSoFar) 0 arr

请注意 foldl 如何为我完成整个迭代部分,我只需要提供过程的“核心”——即如何将“当前元素”x 与“到目前为止积累的状态”sumSoFar.

最后,您可能会注意到 \x y -> x + y 等同于 (+) 并将其重写得更短:

sum :: Array Number -> Number
sum arr = foldl (+) 0 arr

使用 Data.Foldable 中的 sum

import Data.Foldable
import Data.Int (toNumber)
mean :: Array Number -> Number
mean x = sum x / toNumber (length x)
y = mean [1.0, 2.0, 3.0]

或者,使用同一模块中的 foldl,这暗示了如何编写更通用的代码:

import Data.Foldable
import Data.Int (toNumber)
mean :: Array Number -> Number
mean x = foldl (+) 0.0 x / toNumber (length x)
y = mean [1.0, 2.0, 3.0]