Functional programming 使用以前的值生成序列
我正在学习用F#进行函数编程,我想写一个函数,为我生成一个序列 有一个用于转换值的预定函数,在我需要写入的函数中,应该有两个输入——起始值和序列长度。序列从初始值开始,下面的每一项都是将转换函数应用于序列中的前一个值的结果 在C#中,我通常会这样写:Functional programming 使用以前的值生成序列,functional-programming,f#,Functional Programming,F#,我正在学习用F#进行函数编程,我想写一个函数,为我生成一个序列 有一个用于转换值的预定函数,在我需要写入的函数中,应该有两个输入——起始值和序列长度。序列从初始值开始,下面的每一项都是将转换函数应用于序列中的前一个值的结果 在C#中,我通常会这样写: public static IEnumerable<double> GenerateSequence(double startingValue, int n) { double TransformValue(double x)
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,我竖起大拇指,因为我的背景在命令式筒仓中,所以也更容易推理。