使用先前的值生成序列

时间:2019-02-02 17:07:28

标签: functional-programming f#

我正在学习使用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个元素,我必须对transformValue函数进行另外的99次调用,而不仅仅是进行一次调用(就像我在C#实现中所做的那样)。这种表情非常糟糕。

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


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

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

2 个答案:

答案 0 :(得分:6)

@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即可,或者在第二种情况下,请删除if

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。变异可能通常是“邪恶的”,但在这种情况下,它是一个局部可变变量,不会以任何方式破坏参照透明性,因此我认为这没有害处。

答案 1 :(得分:3)

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

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

  1. Seq.unfold期望从现在开始将称为f的转换函数返回 option 。如果序列应结束,则应返回None;如果序列中还有另一个值,则应返回Some (...)。如果您从不返回None,则可以这种方式创建无限序列;无限序列非常好,因为F#懒惰地评估序列,但是您确实需要注意不要循环遍历整个无限序列。 :-)

  2. Seq.unfold还希望,如果f返回Some (...),它将不仅返回下一个值,还返回下一个值和下一个状态的元组。这在文档的Fibonacci示例中显示,其中状态实际上是当前值和前一个值的元组,它们将用于计算所示的 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.fold还是Seq.unfold的最简单方法是问问自己,这两个语句中的哪一个是正确的:

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

  2. 我有一个初始值和一个从 current 值产生 next 值的函数,我想以一个列表结尾(或值的序列或数组)。这是一个展开操作:我取一个小的起始值,然后将其“扩展”(可以这么说),直到它是一个完整的值列表为止。