这本书并不打算介绍Perl模块的内部原理,但Memoize内部应用的一些技术和稍后要讨论的内容有直接的关系,所以现在简单介绍一下。
Memoize的参数为函数名(或者函数引用)。它生成一个新的函数用于维护缓存并在缓存中查找它的参数。如果新函数在缓存中找到了参数,就直接返回缓存的值,否则就调用原始函数,将返回值保存在缓存中之后再将它返回给调用者。
生成新函数之后,Memoize就将它安装到Perl的符号表中,代替原来的函数。这样,你以为你是在调用原始函数,实际上调用的是新的缓存管理函数。
真正的Memoize模块的代码有350行之多,无法深入研究它的内部细节,因此只就一个小型的memoizer[TODO]函数简化版进行讨论。省略的部分中,最重要的是处理Perl符号表的代码(此处我们手动处理符号表)。要讨论的memoize函数的参数是需要memoize的函数引用,函数返回值是memoize过的函数引用,即指向缓存管理函数的引用:
sub memoize {
my ($func) = @_;
my %cache;
my $stub = sub {
my $key = join ',', @_;
$cache{$key} = $func->(@_) unless exists $cache{$key};
return $cache{$key};
};
return $stub;
}
调用方法为,首先执行:
$fastfib = memoize(\&fib);
此时$fastfib就是fib()被memoized之后的函数。接下来执行*fib = memoize(\&fib)可以将memoized后的函数安装到符号表中,替换原始函数。本例中这一步是必须的,否则就无法快速计算斐波那契数。仅仅创建memoized的fib()函数是不够的,因为fib()内部的递归调用还在调用名为fib()的函数,如果不给*fib复制,那么fib就还是原始的不带缓存的版本,速度很慢。
memoize的工作原始是什么呢?memoize首先接受fib的引用,并创建一个私有的%cache变量来保存缓存数据。然后生成一个stub函数,临时保存到$stub中,再返回给调用者。这个stub函数就是被memoized之后的fib。本例中,调用memoize的函数得到这个stub函数的引用之后,将其保存到$fastfib中。
调用$fastfib时,实际上调用的是之前memoize生成的stub函数。它将函数参数用逗号连接起来组成散列的键,然后在缓存中查找有无相同的键。如果有,stub函数就直接返回缓存的值。
如果散列中没有指定的键,stub函数就通过$func->(@_)的方式调用原始函数,得到结果后保存到缓存中,再返回(参见图 3.1 “调用被memoized的函数的过程。”)。
有几个细微之处需要解释一下。首先,假设调用了memoize(\&fib),得到返回值$fastfib。然后调用$fastfib,而它又使用了$func。人们经常会问,为什么$func在memoize返回之后仍然有效?
这个问题表明了通常人们对作用域的误解。变量由两部分组成:变量名和变量值。
[9]
将值和名称关联起来,就得到了一个变量。这种关联称为绑定,也说该变量名被绑定到了值上。
在memoize返回之后再使用$func可能会产生两个问题:值有可能不存在了,或者绑定关系发生了改变,导致变量名指向了错误的值,或什么都不指向。
作用域(scope)是某个变量绑定有效的那段程序代码。在绑定的作用域内,名称和值之间有关联;出了这个作用域,绑定就会脱离作用域,名称和值之间就不再相关。名称可能有其他意义,或者完全没有意义。
进入memoize后,my $func定义创建了一个新的标量值,并为它绑定了名称$func。名称的作用域是用my定义的,那么$func的有效范围从my定义语句之后开始,直到最小的语句块结束。本例中,最小的语句块就是标有sub memoize的语句块。在这个块中,$func指向刚刚创建的词法变量;在块之外,它指向其他内容,可能是毫无关联的全局变量$func。由于调用$func的stub函数位于该语句块之内,所以并不存在作用域问题——在stub函数内,$func仍然处于作用域之内,$func这个名称仍有正确的绑定。
离开sub memoize块以后,$func就有了其他的意义,但是stub函数位于块内,而不是块外。注意作用域是词法的,就是说它是静态程序文本的属性,而不是代码执行顺序的属性。stub函数在sub memoize语句块之外被调用的这个事实与它毫无关系,它的代码“从物理上”位于$func绑定的作用域之内。
%cache的情况与此完全相同。