假设我想计算整数的阶乘。 F#中的一个简单方法是:
let rec fact (n: bigint) =
match n with
| x when x = 0I -> 1I
| _ -> n * fact (n-1I)
但是,如果我的程序需要动态编程,那么在使用memoization的同时如何维持函数式编程?
我对此有一个想法是制作一系列懒惰元素,但我遇到了一个问题。假设以下代码在F#中是可接受的(它不是):
let rec facts =
seq {
yield 1I
for i in 1I..900I do
yield lazy (i * (facts |> Seq.item ((i-1I) |> int)))
}
F#中有什么类似的想法吗? (注意:我知道我可以使用.NET Dictionary而不是调用“.Add()”方法命令式样式?)
另外,有什么办法可以用函数来概括这个吗?例如,我可以创建一个由函数定义的collatz function长度序列:
let rec collatz n i =
if n = 0 || n = 1 then (i+1)
elif n % 2 = 0 then collatz (n/2) (i+1)
else collatz (3*n+1) (i+1)
答案 0 :(得分:9)
如果你想懒洋洋地做,这是一个很好的方法:
let factorials =
Seq.initInfinite (fun n -> bigint n + 1I)
|> Seq.scan ((*)) 1I
|> Seq.cache
Seq.cache
表示您不会重复评估您已经枚举过的元素。
然后,您可以使用例如特定数量的因子Seq.take n
,或使用Seq.item n
获取特定的因子。
答案 1 :(得分:2)
首先,我在你的例子中没有看到你对“动态编程”的意思。
使用记忆并不意味着某些东西不具有“功能性”或打破不变性。重要的 关键不在于如何实施。重要的是它的行为方式。一个使用的功能 一个可变的memoization仍然被认为是纯粹的,只要它的行为就像一个纯函数/不可变的 功能。因此,在调用者看不到的有限范围内使用可变变量仍然是 被认为是纯粹如果实现很重要,我们也可以考虑尾递归 不纯,因为编译器将它转换为带有可变变量的循环。那里 还存在一些List.xyz函数,它使用变异并将事物转换为可变变量 只是因为速度。这些函数仍被认为是纯/不可变的,因为它们仍然表现得像 纯粹的功能。
序列本身已经很懒惰了。它只有在你要求这些元素时才会计算它的所有元素。 因此,创建一个返回惰性元素的序列对我来说没有多大意义。
如果你想加快计算速度,有多种方法可以做到这一点。即使在递归中 你可以使用一个传递给下一个函数调用的累加器。而不是做深 递归。
let fact n =
let rec loop acc x =
if x = n
then acc * x
else loop (acc*x) (x+1I)
loop 1I 1I
整体与
相同let fact' n =
let mutable acc = 1I
let mutable x = 1I
while x <= n do
acc <- acc * x
x <- x + 1I
acc
只要你学习函数式编程,最好习惯第一个版本并学习 了解循环和递归如何相互关联。但除了学习之外,你没有理由 总是应该强迫自己总是写第一个版本。最后你应该使用你更多的考虑 可读且易于理解。不是某些东西是否使用可变变量作为实现。
最终没有人真正关心确切的实施。我们应该将功能视为黑盒子。所以只要 一个函数就像一个纯函数,一切都很好。
以上使用累加器,因此您无需再次重复调用函数来获取值。你也是 不需要内部可变缓存。如果你真的有一个缓慢的递归版本,并希望加快速度 缓存你可以使用类似的东西。
let fact x =
let rec fact x =
match x with
| x when x = 1I -> 1I
| x -> (fact (x-1I)) * x
let cache = System.Collections.Generic.Dictionary<bigint,bigint>()
match cache.TryGetValue x with
| false,_ ->
let value = fact x
cache.Add(x,value)
value
| true,value ->
value
但是,随着带累加器的版本,这可能会更慢。如果您希望将调用缓存到多个事件中 事实上,您需要一个外部缓存来调用整个应用程序。您可以在事实之外创建一个词典并使用 私有变量。但是,您也可以使用带闭包的函数,并使整个过程本身通用。
let memoize (f:'a -> 'b) =
let cache = System.Collections.Generic.Dictionary<'a,'b>()
fun x ->
match cache.TryGetValue x with
| false,_ ->
let value = f x
cache.Add(x,value)
value
| true,value ->
value
let rec fact x =
match x with
| x when x = 1I -> 1I
| x -> (fact (x-1I)) * x
所以现在你可以使用类似的东西。
let fact = memoize fact
printfn "%A" (fact 100I)
printfn "%A" (fact 100I)
并从每个其他带有1个参数的函数中创建一个memoized函数
请注意,memoization不会自动加速一切。如果你在事实上使用memoize函数
什么都没有加速,如果没有记忆,它甚至会变慢。您可以添加printfn "Cache Hit"
到memoize函数内的| true,value ->
分支。连续两次调用fact 100I
只会
产生一个“Cache Hit”行。
问题在于算法的工作原理。它从100I开始,下降到0I。所以计算100I问 99I的缓存,它不存在,所以它试图计算98I并询问缓存。那也不存在 所以它下降到1I。它总是询问缓存,从未找到结果并计算所需的值。 所以你永远不会得到“缓存命中”,你还有额外的工作要求缓存。要真正受益于 缓存你需要改变事实本身,所以它从1I开始到100I。当前版本甚至抛出StackOverflow 对于大输入,即使使用memoize功能。
只有第二个呼叫才能从缓存中受益,这就是为什么两次调用fact 100I
只能打印一次“Cache Hit”。
这只是一个容易让缓存/ memoization行为错误的例子。一般来说,你应该尝试 写一个函数,因此它是尾递归的,而是使用累加器。不要尝试编写期望的函数 记忆正常工作。
我会选择一个带累加器的解决方案。如果你描述了你的应用程序,你发现这仍然很慢
并且您的应用程序存在瓶颈,缓存fact
会有所帮助,那么您也可以只缓存结果
facts
直接。像这样的东西。您可以使用dict
或地图。
let factCache = [1I..100I] |> List.map (fun x -> x,fact x) |> dict
let factCache = [1I..100I] |> List.map (fun x -> x,fact x) |> Map.ofList