10.1.2.1 C# 和 F# 中可重用的记忆化

如果看一下清单 10.3 中建立 add 值的代码,可以发现,它并不真正知道加法,只是使用了 addSimple 函数,因此,也可以处理其他任何函数。为了使代码更通用,我们可以把这个函数改成参数。

我们要写一个函数(C# 中叫方法),参数为函数,返回这个函数的记忆化版本。参数值是做实际工作的函数,返回的函数增加了缓存功能。清单 10.4 是C# 版本的代码。

清单 10.4 泛型记忆化方法 (C#)

Func<T, R> Memoize<T,R>(Func<T, R> func) {   [1]

var cache = new Dictionary<T,R>();   <-- 由闭包捕获缓存

return arg => {

R val;

if(cache.TryGetValue(arg, out val)) return val;   <-- 返回缓存值

else {

val =func(arg);      | 计算值,

cache.Add(arg, val);  | 加到缓存中

returnval;          |

} };

}

代码类似于清单 10.3 中的专用加法函数;另外,我们首先创建一个缓存,然后,返回在闭包中捕捉缓存的lambda 函数。这样,对于每个返回的函数,将只有一个缓存,这正是我们想要的。

方法签名[1]表明,参数为函数 Func<T, R>,返回相同类型的函数。这样,不改变函数的结构,只是把它包装成另一个实现缓存的函数。签名是泛型的,所以,它可以使用任何接受一个参数的函数;用元组就能突破这个限制。下面的代码实现了两个数字相加的记忆化函数的 C# 版本:

var addMem = Memoize((Tuple<int, int>arg) => {

Console.Write("adding {0} +{1}; ", arg.Item1, arg.Item2);

return arg.Item1 + arg.Item2; });

Console.Write("{0}; ",addMem(Tuple.Create(19, 23)));   |[1]

Console.Write("{0}; ",addMem(Tuple.Create(19, 23)));  |

Console.Write("{0}; ", addMem(Tuple.Create(18,24)));    [2]

运行这段代码,会发现第二段代码只输出“adding 19+23”一次,第三块输出“adding 18 + 24”。这就是说,前面的加法只执行一次,因为,缓存比较两个元组值,当它们的元素都相等时,就找到了一个匹配。这不会处理元组的第一个实现,因为,它没有任何相等的值实现;在 .NET 4.0 中的元组类型,以及本章源代码都是重写(override)了Equals 方法,进行组件值的比较,这称为结构比较(structural comparison),我们会在第十一章详细介绍。要使 Memoize 方法能够处理有多个参数的函数,还有一种选择,重载
Func<T1, T2, R>、Func<T1, T2, T3, R>,等等。在缓存中,仍然把元组作为键,但对于方法的用户来说,它是隐藏的。

[

overload和override的区别:

override(重写)

1、方法名、参数、返回值相同。

2、子类方法不能缩小父类方法的访问权限。

3、子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。

4、存在于父类和子类之间。

5、方法被定义为final不能被重写。

overload(重载)

1、参数类型、个数、顺序至少有一个不相同。

2、不能重载只有返回值不同的方法名。

3、存在于父类和子类、同类中。

]

在 F# 中,实现的泛型同样代码更加容易。我们将取清单 10.3 中写的加法代码,把计算的函数作为记忆化函数的参数。可以在清单 10.5 中看到 F# 版本。

清单10.5 泛型记忆化方法(F# Interactive)

> let memoize(f) =

let cache = newDictionary<_, _>()   <-- 初始化由闭包捕获的缓存

(fun x –>

matchcache.TryGetValue(x) with

|true, v –> v

| _-> let v = f(x)

cache.Add(x, v)

v);;

val memoize : (‘a -> ‘b) -> (‘a ->‘b)    [1]

在 F# 版本中,可以推断出类型签名[1],所以,我们不必要手工使函数成为泛型。F# 编译器使用泛型化(generalization)就能做到,推断出的签名相当于 C# 代码中显式签名。

这一次,我们将使用更有意义的例子来演示记忆化的有效。我们会回到全世界最喜欢的递归示例:阶乘函数。清单 10.6 试图对这个函数进行记忆化(memoize),但并不完全按照计划......

清单10.6 用实现记忆化的递归函数的困难 (F# Interactive)

> let rec factorial(x) =     [1]

printf"factorial(%d); " x

if (x <= 0)then 1 else x * factorial(x - 1);;  [2]

val factorial : int –> int

> let factorialMem = memoizefactorial   [3]

val factorial : (int -> int)

> factorialMem(2);;

factorial(2); factorial(1);factorial(0);   <-- 第一次计算 2!

val it : int = 1

> factorialMem(2);;

val it : int = 1    <-- 使用缓存的值

> factorialMem(3);;

factorial(3); factorial(2); factorial(1);factorial(0)   [4] 为什么这个 2! 会重新计算

val it : int = 2

乍一看,代码似乎是正确的。首先把阶乘计算实现为简单的递归函数[1],然后,使用memoize(记忆化)函数,创建函数的优化版本[3]。我们在后面进行测试,运行相同的调用两次,看上去正常。第一次调用后,结果被缓存,因此,可以重复使用。

最后一个调用[4]就不正确了,或更确切地说,没有按照我们所希望的。问题出在记忆化只覆盖了第一次调用,即 factorialMem(3)。factorial 函数在随后的递归计算期间,直接调用了原始的函数,没有调用记忆化的版本。要解决这个问题,需要把进行递归调用的这一行代码[2],改成使用记忆化的版本(factorialMem)。这个函数在后面的代码中声明,所以,可以使用 let rec ... and ... 语法来声明两个相互递归的函数。

还有一个简单的方法,就是使用 lambda 函数,只把记忆化的版本公开为可重用的函数。清单 10.7 只用几行代码实现这个函数。

清单10.7改正后可记忆化的 factorial 函数 (F# Interactive)

> let rec factorial = memoize(fun x->

printfn"Calculating factorial(%d)" x

if (x <= 0)then 1 else x * factorial(x - 1));;    [1]

warning FS0040: This and other recursivereferences to the        |

object(s) being defined will be checked forinitializationsoundness  | [2]

at runtime through the use of a delayedreference...             |

val factorial : (int -> int)

> factorial(2);;

factorial(2); factorial(1);factorial(0);   <-- 先计算几个值

val it : int = 2

> factorial(4);;

factorial(4); factorial(3);   <-- 只计算没有的值

val it : int = 24

在这个例子中的 factorial 表示值,它不是语法意义上定义为有参数的函数,相反,它就是值(恰好是函数类型),由 memoize 函数返回的。这就是说,我们并没有声明递归的函数,而是声明递归的值。我们在第八章创建决策树时,用 let rec声明递归值,但是,我们只用它来以更自然的方式写节点,在代码中并没有任何的递归调用。

这一次,我们创建了真正递归的值,因为,值 factorial 在声明它自己的时候就用到了。递归值的困难在于,如果我们不小心,写出引用某个值的代码,可能会在这个值的初始化期间,是无效的操作。下面是不正确初始化的例子:

let initialize(f) = f()

let rec num = initialize (fun _ -> num +1)

在这里,对值 num 的引用出现在 lambda 函数的内部,在初始化期间被调用,当 initialize 函数被调用时,lambda 函数被调用。如果运行这个代码,在 num 被声明的地方,会出现运行时错误。使用递归函数时,函数总是在执行递归调用的时候,才定义;代码可能永远保持循环,但是,这是不同的问题。

我们声明的 factorial,对 factorial 值的引用出现在 lambda 函数中,并不在初始化期间调用,所以,是有效的声明。而F# 编译器在编译时无法区分这两种情况,所以,会发出警告[2],并增加了运行时检查。不要过于害怕这个!只要确保包含引用的 lambda 函数不在初始化过程中进行计算,就行了。

由于 factorial 声明进行递归调用时,使用记忆化版本,现在可以在任意计算步骤中,读缓存的值。例如,当我们计算 2 的阶乘后,再计算 4 的阶乘,只需要计算剩下的两个值。

注意

到目前为止,我们已经看到了函数编程中的两种优化方法。使用尾递归,要吧避免栈溢出,写出更好的递归函数;记忆化,能够优化任何无副作用的函数。

这两种方法都非常完美地适合迭代开发的风格,被认为是 F# 编程的重要方面。我们可以从简单的实现开始,往往是一个函数,可能是递归的,无副作用。在随后的过程中,对代码中需要优化的地方进行确认。正如我们前面看到的,演变代码的结构,添加面向对象方面,都很容易,优化所需要的改变也相当简单。迭代过程能够在复杂的领域花很小的代价,效益显著。

到目前为止,我们已经看到写高效函数的通用技巧;还有一种数据结构,本身适合于非常具体的优化:集合类型。在下一节,我们要讨论函数式列表,以及以函数方式使用 .NET 数组。

时间: 2024-10-10 07:56:18

10.1.2.1 C# 和 F# 中可重用的记忆化的相关文章

10.1 优化函数 在前面的章节中,我们已经知道,递归是 F# 中处理函数的主要控制流机制。我们第一次是使用它写一些进行计算的简单函数,例如,计算指定范围内的数字的和或阶乘。后来,我们发现它在处理递

10.1 优化函数 在前面的章节中,我们已经知道,递归是 F# 中处理函数的主要控制流机制.我们第一次是使用它写一些进行计算的简单函数,例如,计算指定范围内的数字的和或阶乘.后来,我们发现它在处理递归数据结构,最重要的列表是时,是无价的. 我们知道,递归也有一些局限性,堆栈溢出的可能性是最明显的一个:我们将会看到,某些递归计算非常低效.在命令式语言中,通常使用非递归函数,以避免出现问题:函数语言已经有方法解决这些问题,并可以高效地使用递归.首先要集中关注于正确性:如果一个额外的字节吹动堆栈,真正

8.1.4 在 F# 中使用函数列表

首先,我们声明一个表示有关客户信息的类型:客户有很多属性,因此,用F# 的记录类型表示最自然的选择,我们在前一章已经看过.清单 8.4 显示了类型声明,和所创建样本客户的代码. 清单 8.4 Client 记录类型和样本值 (F# Interactive) > type Client = { Name : string; Income : int;YearsInJob : int UsesCreditCard : bool;CriminalRecord : bool };; type Clien

11.3.1.1 C# 和 F# 中的提前计算

在大多数主流的语言中,指定计算顺序的规则很简单:程序进行函数调用时,先计算所有的参数值,然后再执行函数.我们用前面的例子来演示: TestAndCalculate(Calculate(10)); 在所有的主流语言中,程序都会执行Calculate(10),然后再把结果作为参数值传递给TestAndCalculate.正如我们在前面的示例中已经看到的,如果函数 TestAndCalculate 不需要这个参数值,就很不幸了:在这种情况下,我们就无故地浪费了一些CPU 周期!这种策略称为提前计算策略

s?h?e?l?l?中?条?件?判?断?i?f?中?的?-?z?到?-?d?

shell中条件判断if中的-z到-d的意思 2011-09-05 10:30 [ -a FILE ] 如果 FILE 存在则为真. [ -b FILE ] 如果 FILE 存在且是一个块特殊文件则为真. [ -c FILE ] 如果 FILE 存在且是一个字特殊文件则为真. [ -d FILE ] 如果 FILE 存在且是一个目录则为真. [ -e FILE ] 如果 FILE 存在则为真. [ -f FILE ] 如果 FILE 存在且是一个普通文件则为真. [ -g FILE ] 如果 F

C语言 有一个整形数组a,有10个元素,要求输出数组中的全部元素

<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">有一个整形数组a,有10个元素,要求输出数组中的全部元素</span> 解题思路:引用数组中各元素的值有3种方法:1.下标法,如a[3];2.通过数组名计算数组元素的地址,找出元素的值 3.用指针变量指向数组元素. //用指针变量指向数组元素 #include <std

9.4.2.2 F# 中的向上转换和向下转换(UPCASTS AND DOWNCASTS)

9.4.2.2 F# 中的向上转换和向下转换(UPCASTSAND DOWNCASTS) 如果类型之间的转换不会失败,就称为向上转换(upcast).我们已经看到,把类型转换成由该类型实现的接口,就是这种情况:另一个示例是把派生类转换成它的基类,在这种情况下,编译器也可以保证操作是正确的,不会失败. 如果有一个基本类型的值,希望将它转换为继承类,操作可能会失败,因为基类的值可能是目标类的值,也可能不是.在这种情况下,我们必须使用第二种类型转换,称为向下转换(downcast).让我们用一个示例来

F#中的隐式转换

我们知道隐式变换在可控情况下会使代码变得简洁.熟悉C#的都知道C#中可以自定义隐式变换,例如 public class A { private int data; public static implicit operator A(int i) { return new A{ data = i}; } } 众所周知,F#本身不会进行任何隐式变换.那F#中是否也可以自定义隐式变换呢? 当然可以.通过定义自己的操作符即可实现. 我们先定义一个转换操作符 let inline (!>) (x:^a)

6.6.1 F# 中函数调用的类型推断

虽然,在 F# 中可以用尖括号指定类型参数值,与 C# 中的方式相同,但这种方法很少使用.原因是,当编译器无法推断出所有的信息,需要程序员的帮助时,我们仅在真正需要的地方,添加类型批注.我们用一个示例来演示: > Option.map (fun dt -> dt.Year)(Some(DateTime.Now));; error FS0072: Lookup on object ofindeterminate type. > Option.map (fun (dt:DateTime) -

12.5.3 在 F# 中实现计算生成器

在计算表达式块前面的标识符,是类的实例,把所需的操作实现成为实例成员.许多操作都已经有了,我们根本不必要提供所有的,最基本的操作用 Bind 和 Return 成员实现.当 F# 编译器看到计算表达式,比如清单 12.18 时,会把它转换为使用这些成员的F# 代码.F# 示例转换成这样: value.Bind(ReadInt(), fun n –> value.Bind(ReadInt(), fun m–> let add = n + m let sub = n – m value.Retur