Functional programming 使用以前的值生成序列

Functional programming 使用以前的值生成序列,functional-programming,f#,Functional Programming,F#,我正在学习用F#进行函数编程,我想写一个函数,为我生成一个序列 有一个用于转换值的预定函数,在我需要写入的函数中,应该有两个输入——起始值和序列长度。序列从初始值开始,下面的每一项都是将转换函数应用于序列中的前一个值的结果 在C#中,我通常会这样写: public static IEnumerable<double> GenerateSequence(double startingValue, int n) { double TransformValue(double x)

我正在学习用F#进行函数编程,我想写一个函数,为我生成一个序列

有一个用于转换值的预定函数,在我需要写入的函数中,应该有两个输入——起始值和序列长度。序列从初始值开始,下面的每一项都是将转换函数应用于序列中的前一个值的结果

在C#中,我通常会这样写:

public static IEnumerable<double> GenerateSequence(double startingValue, int n)
{
    double TransformValue(double x) => x * 0.9 + 2;

    yield return startingValue;

    var returnValue = startingValue;
    for (var i = 1; i < n; i++)
    {
        returnValue = TransformValue(returnValue);
        yield return returnValue;
    }
}
这种实现存在两个明显的问题

首先是因为我试图避免生成可变值(类似于C#实现中的
returnValue
变量),所以在生成序列时没有重用以前计算的值。这意味着对于序列的第100个元素,我必须额外调用99个
transformValue
函数,而不是仅仅调用一个(就像我在C#实现中所做的那样)。这台机器的性能非常差

第二个是整个函数似乎不是按照函数编程编写的。我非常确信有更优雅和紧凑的实现。我怀疑这里应该使用
Seq.fold
List.fold
之类的东西,但我仍然无法掌握如何有效地使用它们


所以问题是:如何在F#中重新编写
GenerateSequence
函数,使其采用函数式编程风格并具有更好的性能


任何其他建议也将受到欢迎。

您对问题的描述非常好:“序列从初始值开始,下面的每一项都是将转换函数应用于序列中的前一个值的结果。”

这是对
Seq.unfold
方法的完美描述。它接受两个参数:初始状态和转换函数,并返回一个序列,其中每个值都是从前一个状态计算出来的。使用
Seq.unfold
有一些微妙之处,可能无法很好地解释:

  • Seq.unfold
    期望转换函数返回一个选项,从现在起我将调用
    f
    。如果序列应该结束,它应该返回
    None
    ;如果序列中还有其他值,它应该返回
    Some(…)
    。如果从不返回
    None
    ,则可以通过这种方式创建无限序列;无限序列是非常好的,因为F#对序列的求值是惰性的,但是您需要小心,永远不要在整个无限序列上循环。:-)

  • Seq.unfold
    还期望如果
    f
    返回
    Some(…)
    ,它将不仅返回下一个值,而且返回下一个值和下一个状态的元组。文档中的Fibonacci示例显示了这一点,其中状态实际上是当前值和上一个值的元组,用于计算所示的下一个值。文档示例没有很清楚地说明这一点,因此我认为下面是一个更好的示例:

    let infiniteFibonacci = (0,1) |> Seq.unfold (fun (a,b) ->
        // a is the value produced *two* iterations ago, b is previous value
        let c = a+b
        Some (c, (b,c))
    )
    infiniteFibonacci |> Seq.take 5 |> List.ofSeq // Returns [1; 2; 3; 5; 8]
    let fib = seq {
        yield 0
        yield 1
        yield! infiniteFibonacci
    }
    fib |> Seq.take 7 |> List.ofSeq // Returns [0; 1; 1; 2; 3; 5; 8]
    
  • 为了回到你的
    生成序列
    问题,我会这样写:

    let GenerateSequence startingValue n =
        let transformValue x =
            let result = x * 0.9 + 2.0
            Some (result, result)
        startingValue |> Seq.unfold transformValue |> Seq.take n
    
    let generateSequenceMutable startingValue n = seq {
      let transformValue x = x * 0.9 + 2.0
      let mutable returnValue = startingValue
      for i in 1 .. n  do 
        yield returnValue 
        returnValue <- transformValue returnValue }
    
    let generateSequenceRecursive startingValue n = 
      let transformValue x = x * 0.9 + 2.0
      let rec loop value i = seq {
        if i < n then
          yield value
          yield! loop (transformValue value) (i + 1) }
      loop startingValue 0
    
    或者,如果需要在序列中包含起始值:

    let GenerateSequence startingValue n =
        let transformValue x =
            let result = x * 0.9 + 2.0
            Some (result, result)
        let rest = startingValue |> Seq.unfold transformValue |> Seq.take n
        Seq.append (Seq.singleton startingValue) rest
    
    顺序折叠和顺序展开之间的差异 记住是否要使用
    Seq.fold
    Seq.unfold
    的最简单方法是问问自己这两种说法中哪一种是正确的:

  • 我有一个项目列表(或数组或序列),我希望通过对列表中的项目对重复运行计算来生成单个结果值。举个例子,我想取这一系列数字的乘积。这是一个折叠操作:我获取一个长列表并“压缩”它(可以这么说),直到它成为一个值

  • 我有一个单一的起始值和一个从当前值生成下一个值的函数,我希望最终得到一个值列表(或序列或数组)。这是一个展开操作:我取一个小的起始值并“展开”它(可以说),直到它成为一个完整的值列表


  • @rmunn的答案显示了一个相当不错的解决方案,使用
    unfold
    。我认为还有两种选择值得考虑,它们实际上只是使用可变变量和递归序列表达式。选择可能是个人偏好的问题。其他两个选项如下所示:

    let GenerateSequence startingValue n =
        let transformValue x =
            let result = x * 0.9 + 2.0
            Some (result, result)
        startingValue |> Seq.unfold transformValue |> Seq.take n
    
    let generateSequenceMutable startingValue n = seq {
      let transformValue x = x * 0.9 + 2.0
      let mutable returnValue = startingValue
      for i in 1 .. n  do 
        yield returnValue 
        returnValue <- transformValue returnValue }
    
    let generateSequenceRecursive startingValue n = 
      let transformValue x = x * 0.9 + 2.0
      let rec loop value i = seq {
        if i < n then
          yield value
          yield! loop (transformValue value) (i + 1) }
      loop startingValue 0
    

    如果我写这篇文章,我可能会使用可变变量或
    unfold
    。突变可能是“普遍有害的”,但在这种情况下,它是一个局部可变变量,不会以任何方式破坏引用透明度,因此我认为它不会有害。

    这是一个例外的答案!非常感谢你!嘿,托马斯!谢谢你的回答!这是使用
    yield的一个很好的例子,除了我的问题的整体解决方案之外!我想,
    屈服非常整洁:-)。它的另一个优点是它也相当有效——它不会创建一个从另一个序列读取的序列,而从另一个序列读取取而代之的是,它有一个优化功能,可以删除所有的中间序列,因此递归不会产生巨大的成本。对这个Tomas,我竖起大拇指,因为我的背景在命令式筒仓中,所以也更容易推理。