我正在学习使用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.fold
或List.fold
或类似的东西,但是我仍然无法掌握如何有效地使用它们。
所以问题是:如何用F#重写GenerateSequence
函数,使其具有函数式编程风格并具有更好的性能?
任何其他建议也将受到欢迎。
答案 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可能无法很好地解释:
Seq.unfold
期望从现在开始将称为f
的转换函数返回 option 。如果序列应结束,则应返回None
;如果序列中还有另一个值,则应返回Some (...)
。如果您从不返回None
,则可以这种方式创建无限序列;无限序列非常好,因为F#懒惰地评估序列,但是您确实需要注意不要循环遍历整个无限序列。 :-)
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
的最简单方法是问问自己,这两个语句中的哪一个是正确的:
我有一个项目列表(或数组或序列),我想通过对列表中的成对项目重复运行计算来产生单个结果值。例如,我想取这一系列数字的乘积。这是一个 fold 操作:我拿了一个长列表并将其“压缩”(可以这么说),直到它成为单个值为止。
我有一个初始值和一个从 current 值产生 next 值的函数,我想以一个列表结尾(或值的序列或数组)。这是一个展开操作:我取一个小的起始值,然后将其“扩展”(可以这么说),直到它是一个完整的值列表为止。