F#中的递归序列

时间:2016-02-08 23:57:28

标签: recursion dynamic f# functional-programming memoization

假设我想计算整数的阶乘。 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)

2 个答案:

答案 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