【问题标题】:Generate sequence using previous values使用先前的值生成序列
【发布时间】:2019-02-03 06:40:07
【问题描述】:

我正在学习使用 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;
    }
}

当我试图将这个函数翻译成 F# 时,我做了这个:

let GenerateSequence startingValue n =
    let transformValue x =
        x * 0.9 + 2.0

    seq {
        let rec repeatableFunction value n = 
            if n = 1 then
                transformValue value
            else 
                repeatableFunction (transformValue value) (n-1)

        yield startingValue
        for i in [1..n-1] do
            yield repeatableFunction startingValue i
    }

这个实现有两个明显的问题。

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

第二是整个函数好像不是按照函数式编程来写的。我很确定会有更优雅和紧凑的实现。我怀疑这里应该使用Seq.foldList.fold 或类似的东西,但我仍然无法掌握如何有效地使用它们。


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

也欢迎任何其他建议。

【问题讨论】:

    标签: functional-programming f#


    【解决方案1】:

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

    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
    

    我稍微修改了你的逻辑,这样我就不必yield 两次——我只需要再执行一步迭代并在更新值之前产生。这使得generateSequenceMutable 函数非常简单易懂。 generateSequenceRecursive 使用递归实现了相同的逻辑,也相当不错,但我觉得不太清楚。

    如果您想使用其中一个版本并生成一个无限序列,然后您可以从中获取任意数量的元素,您可以在第一种情况下将 for 更改为 while 或删除 @987654328 @第二种情况:

    let generateSequenceMutable startingValue n = seq {
      let transformValue x = x * 0.9 + 2.0
      let mutable returnValue = startingValue
      while true do
        yield returnValue 
        returnValue <- transformValue returnValue }
    
    let generateSequenceRecursive startingValue n = 
      let transformValue x = x * 0.9 + 2.0
      let rec loop value i = seq {
        yield value
        yield! loop (transformValue value) (i + 1) }
      loop startingValue 0
    

    如果我正在写这篇文章,我可能会使用可变变量或unfold。突变可能“通常是邪恶的”,但在这种情况下,它是一个本地化的可变变量,不会以任何方式破坏参照透明度,所以我认为它没有害处。

    【讨论】:

    • 嗨,托马斯!感谢你的回答!这是使用yield! 的一个很好的例子,除了我的问题的整体很好的解决方案!
    • 我认为yield! 真的很简洁:-)。关于它的额外整洁的事情是它也相当有效 - 它不会创建从另一个序列读取的序列,该序列从另一个序列 atc 读取。 - 相反,它有一个优化,可以删除所有中间序列,因此递归不会产生巨大的成本。
    • 为这位 Tomas 竖起两个大拇指,对我来说,我的背景处于命令式孤岛中也更容易推理。
    【解决方案2】:

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

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

    1. Seq.unfold 期望转换函数返回一个选项。如果序列应该结束,它应该返回None,或者如果序列中还有另一个值,它应该返回Some (...)。如果您从不返回None,您可以通过这种方式创建无限序列;无限序列非常好,因为 F# 懒惰地评估序列,但你需要小心不要循环整个无限序列。 :-)

    2. Seq.unfold 还期望如果f 返回Some (...),它不仅会返回下一个值,还会返回下一个值和下一个状态的元组。这在文档中的斐波那契示例中显示,其中状态实际上是当前值和前一个值的元组,将用于计算显示的 next 值。文档示例并没有说得很清楚,所以这是我认为更好的示例:

      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]
      

    回到你的GenerateSequence 问题,我会这样写:

    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 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 的区别

    记住您是否要使用Seq.foldSeq.unfold 的最简单方法是问自己这两个陈述中哪一个是正确的:

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

    2. 我有一个起始值和一个从 current 值生成 next 值的函数,我希望得到一个列表(或序列或数组)的值。这是一个展开操作:我取一个小的起始值并“展开”它(可以这么说),直到它成为一个完整的值列表。

    【讨论】:

    • 这是一个特殊的答案!非常感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-20
    • 1970-01-01
    相关资源
    最近更新 更多