先前的记忆器至少有一个严重的问题。它需要把函数的参数转变成一个散列键,它的做法是使用join:
my $key = join ',', @_;这对只带一个参数的函数有效,也对参数不含逗号的函数有效,包括所有的参数是数字的函数。但是如果函数的参数可能包含逗号,它可能失效,因为以下两个调用却计算出了相同的键:
func("x,", "y"); func("x", ",y");第一次调用,返回的值将以键"x,,y"存放在缓存里。当第二次调用时,没有运行真实的函数。而第一次调用的缓存的值将返回。但是函数本可能想要返回一个不同的值的,记忆术的代码混淆了这两个参数列表,导致一次错误的缓存命中。
由于这只会对那些参数包含逗号的函数失效,这可能不是考虑要点。即使函数的参数包含逗号,也有可能有些它们绝不会包含的字符。Perl的特殊变量$;有时用在这里。它通常包含字符#28,即控制-反斜杠字符。如果键生成器使用join $;, @_,它只会在函数的参数包含控制-反斜杠时失效,一般可以确定这点将不会发生。但是经常有一个函数所带的参数会包含任何字符,这些不完整的特技不会可靠地工作。
这个可以被修正,因为总是有一种可靠的方法把任何数据结构,如这样一个参数列表,转换成一个字符串,那么不同的结构变成不同的字符串。
一个策略是使用模块Storable或FreezeThaw把参数列表转换成一个字符串。一个更有效率的策略是使用转义序列:
my @args = @_; s/([\\,])/\\$1/g for @args; my $key = join ",", @args;这里在原始参数的每个逗号或反斜杠前面都插入一个反斜杠,然后用不带反斜杠的逗号连接在一起。先前看到的问题调用也不再是问题了,因为两个参数列表转换成不同的键了:一个是'x,,y',另一个是'x,,y'。(一个练习:为什么在每个反斜杠前面也要像在逗号前面一样放置一个反斜杠呢?)
然而,正确性是用昂贵的性能代价换来的。转义字符代码比简单的join慢得多,大约慢十倍,即使对一个简单的参数列表如(1,2),且它在每次调用函数时都必然会执行的。平常,我们嘲笑那些想用正确性交换速度的人,因为不管找到错误答案的速度多快都没有意义。但这是一个不平常的情况。因为记忆术的唯一目的就是加速一个函数,使开销尽可能小。
采取折中。memoize的默认行为将是快速的,但并不是在所有情况下都正确的。给memoize的用户一个应急出口修正。如果用户不喜欢默认的键生成方法,他们可以提供一个替代者,memoize将使用这个。
改变是简单的:
### Code Library: memoize-norm1 sub memoize { my ($func, $keygen) = @_; my