The way I write Perl programs

Perl程序写多了,便形成一定的开发套路。我写程序的习惯一度很不好,没有自动单元测试,没有版本控制,现在这两个陋习正在改掉,尽管做得仍然不够严谨。

我习惯先把程序写出来。在实现一定的功能之前,只是简单地手工测试一下,版本控制也不做。当程序具备一定规模之后,才做版本控制,然后加测试。很多时候都是凭感觉,犹豫便是开始做版本控制的信号,面对自己熟悉的Emacs和Perl,却不敢敲入哪怕一个字符,这是理智在提醒自己,到Subversion上场的时候了。感觉自己在上版本控制前后写程序的风格也不同,之前是大开大阖,之后是谨小慎微,频繁提交。

人以群分,物以类聚。好习惯也一样,做了一个,就想着做另一个。所以版本控制之后,通常便盘算着怎么做自动单元测试。写测试可不像把程序放入版本控制那么简单,总是要费一番脑筋,对程序做较大的改动,才好写出一个个单元测试来。一个容易测试的程序,很有可能是一个容易理解的程序。我看自己做了测试的程序,往往函数体短小,功能单一。

因为写测试很费脑筋(看来还是功力不够啊),所以完成之后容易厌倦,开发出现停滞,需要休息一段时间才能重燃激情。

The power of returning functions

在学习Common Lisp的间隙,禁不住手痒,写了两个Perl脚本,第一次尝试将函数作为返回值,感觉相当美妙。其实,返回一个函数并不是什么神奇的法术,在C或C++这样的语言里,我们可以返回一个函数指针,该指针指向程序里的某个函数;然而在Perl里面,作为返回值的函数,不需要是已经存在的函数,我们可以动态地创建一个函数,将其返回。

要理解这里面的不同,首先要理解什么是closure。

sub make_prompt {
    my $n = 0;
    return sub {
        ++$n;
        "shell[$n]>";
    }
}

函数make_prompt返回一个匿名函数,该匿名函数的返回值是个shell提示符,每次执行该函数得到的shell提示符都是不同的,类似于CPAN shell,提示符里含有一个数字,表示提示符出现的次数。

my $prompt = make_prompt();
my $p1 = $prompt->(); # shell[1]>
my $p2 = $prompt->(); # shell[2]>

理解函数make_prompt的关键是其局部变量$n在该函数返回后仍然存在,或者更准确地说,那段空间仍然存在,只是$n不再与其绑定(binding)在一起。换句话说,make_prompt返回的不仅仅是个函数,还包括定义这个函数时与其相关的那部分环境(此例中为$n的空间),使得返回函数可以正确执行。

需要注意的是,每次调用make_prompt时的$n是不同的,所以返回的函数之间没有任何依赖关系

my $prompt1 = make_prompt();
my $prompt2 = make_prompt();
my $p11 = $prompt1->(); # shell[1]>
my $p12 = $prompt1->(); # shell[2]>
my $p21 = $prompt2->(); # shell[1]>

可以看出,closure具有某种程度的封装功能。

理解了closure之后,可以学习currying了。我们稍微修改一下make_prompt的定义:

sub make_prompt {
    my $n = shift;
    return sub {
        my $shell = shift;
        my $m = $n++;
        "$shell[$m]>";
    }
}
my $prompt1 = make_prompt(1);
my $promtp2 = make_prompt(100);
my $p11 = $prompt1->("shell"); # shell[1]>
my $p21 = $prompt2->("root"); # root[100]>

现在,通过传给make_prompt不同的参数,其返回的函数的行为也不一样了,第一个从1开始,第二个从100开始。而且通过让返回函数也接受参数,使得shell提示符可以非常个性化。