为什么我们要学/用Perl?

        今天发现我这个博客已经一个多月没有更新了,这个实在和初衷不符,另外项目压身,也是没有办法的事情,不过等这个项目做出来,或许还能写一篇日志留作后人用。


        这篇日志是谈以Linux为开发环境下Perl的必要性,如果是在Windows下,可能Perl也就没有这么必要,而且在Windows下用Perl也有点违和感(不过我开始学Perl的时候倒是在Windows下给自己开发了一些工具,到现在依然常用)。所以从Windows下的开发者的角度来看,或许我这篇日志的视角就略显的老土了。


一、动机

        我本人对Perl的感情是很真挚的,可能是仅次于vim的,碰到了很多问题(在Linux下),我第一直觉就是尝试用Perl来解决,而实际上往往都能在200行内用一个script解决,极大地节省了我的时间。而我在学习和使用Perl的过程中,未免感受到一些鸡肋,我觉得Perl的衰落是由多方面造成的:

  1. 身边压根就没什么人学习/使用Perl。无论是学校bbs的Linux版还是Chinaunix的Linux版和Perl版,基本都没什么人讨论Perl技术,我当初学习Perl的时候也因此感到吃力;
  2. CPAN的衰落。CPAN一开始是业界一个很好的标杆,作为一个平台,能够很迅速地发布自己的开源工具,让社区变得活力四射。这是Python永远所不能及的,但是如今,CPAN上很多包已经没得到维护;
  3. 开发者的转移。Perl上很多开发者都转移到其他语言/脚本语言上;
  4. 脚本语言整体的衰落。从tiobe的排名上可以看出,脚本语言在几年前微电子发展瓶颈的时候都能达到了使用率的高峰,而随着CPU效率限制和多核CPU的发展,并且用户对软件功能要求的提高,脚本语言越来越得不到青睐,反而C这种底层语言又被大家重视起来。这几年的排名主要受手机、平板等消费嵌入式影响,关于这一部分,我希望以后能有时间谈一谈;
  5. Perl自身发展迟缓。Perl的上下文特性也算是一大特色了。和语法受到简化的Python不同,Perl的语法是很复杂的,解析器开发比较困难,因而版本的更新也相对迟缓。这也是Perl6迟迟不能推出的原因之一。

        但是在实践上,Perl的能力依然强大,我这篇日志也算是我对Perl重新壮大的一中希冀吧。


二、Perl和Python

        Perl和Python算是一对冤家了,跟vim和Emacs一样,都互相斗了很多年。但和Emacs不一样,Python我还是用的。但作为一篇赞颂Perl的日志,我还是免不了俗,得数落一下Python:

  1. 各种语法漏洞。和Perl不一样,Python是数学家设计的,因此在语法上显得不那么经得起推敲:
    #!/usr/bin/python
    
    def foo(a=[]):
    	a.append(1)
    	return a
    
    print(foo())
    print(foo())
    print(foo([]))
    print(foo())
    这一简单例子的运行结果就足以证明Python的语法上缺乏考量了:
    hu@forhu:~/test$ python scope.py 
    [1]
    [1, 1]
    [1]
    [1, 1, 1]
    
    Python还有许多对象建立机制和语法分析顺序引起的问题,这使得开发者要不就不使用这种特性,要不就要对这些特性死记硬背,带来了不必要的麻烦。

  2. 如动机所述,各种发行方式,比较麻烦。不过有了github和sourceforge等这些网站,也算是缓解了这个缺点的影响
  3. 这一点是我个人最讨厌的:Python缺乏语法连贯性。这一点被Python2和Python3完美诠释。作为一门流行的语言,有几个特点是绝对躲不掉的:
    • 长得越来越像其他流行的语言;
    • 更新缓慢;
    • 每次更新的同时会增添新特性,且基本维持所有原有特性,只对少数实验性、不合理的特性作删减。

所有的这些特点都是为了减低开发者的学习成本,并且保持已有代码的可维护性。但是Python3很恶心地删除了Python2的很多已经大量被使用的特性(包括一些钩子),并用另外一些方式来实现同样的功能(OO的一些钩子),还对语法进行修改(比如函数后不能没括号了),这使得Python程序变得不那么好长期维护了,严重违反了这门语言设计的初衷。


三、强大的Perl

        这里先对题目的问题作出浅层的回答:在Linux下,我们更多的会碰到和文字相关的问题,这时候我们用Perl的正则表达式可以很方便地完成各种各样的文字处理工作,并完成报表制作,加速开发。而从语言的角度出发,Perl的语义更加统一,且编程的风格可以更加动态、多变。


1. 好像Perl是write-only的?

        我经常听到有人说Perl是write-only的,这个意思是Perl代码的可读性巨差,写完以后就连作者也读不懂了,这也是一般的脚本程序员极力推荐脚本新手学习Python的原因,甚至有人已经被吓到用shell script都不愿意学习Perl了。这个观点是大错特错了。

        正如我上面说过,一门流行语言必然发展得越来越想其他流行的语言,Perl也不例外,从Perl的语法结构来看,它大概像下面这几种语言(或者工具):C, shell script, awk, sed, lisp。如果一个学过shell script的程序员去学Perl,我相信他的第一印象就是:太像了!Perl经常被认为是shell script的延伸,很大的原因就是源代码中无尽的$符号,而且Larry让Perl的语句更接近C,这让新手不会像学习shell script的时候对if语句后面的[ ]感到奇怪。

        但是话又说回来,Perl确实一眼望下去并不是一门勾起人们阅读欲望的语言,而造成这个问题的罪魁祸首我觉得就是正则表达式了。而我经常同用其他脚本的程序员沟通的时候就发现,Perl基本就被等同成正则,而编写Perl程序就是在写正则,所以你写你的正则,最后淹没在bug里去吧。一聊完我就可以下一个结论:这样的开发者,不仅不会Perl,而且还不会正则。正则是强大的,比方说我要如何验证邮箱的合法性?如果是正则表达式,就是一行的语句:


die "invalid email address!\n" unless $email=~m/^([a-zA-Z]\w*\.?\w*)@((?:\w+?\.)+\w+)$/;

my ($name, $domain)= ($1, $2);


        好像这一行就能把人弄蒙了,但是这条语句确实可以检查$email里存放的字符串是否email的合法格式并且在合法的时候向$name和$domain返回对应邮箱的用户名和域名。但是如果不使用正则表达式呢?恐怕就得从NFA设计起了,然后再手工转化成DFA,再用其他什么语言来实现,只是为了完成我1分钟就能完成的工作。

        对于已经正确认识到正则匹配的重要性的开发者,用Perl来编写正则表达式能更加迅速的完成编写工作。Perl的正则表达式有两个特点,一个是扩展性,一个是迅速。就比如说我上面的例子,\w是字符集[a-zA-Z0-9_]的同义词,尽管如此,如果每次都要这样来写一个字符集的话,会很明显降低正则表达式的可读性和可维护性,这时候,\w作为一个内建的特殊字符来实现的功能就很明显了。

        而一个更加使得Perl正则强大的原因,就是Perl的正则支持变量展开和代码内插,让正则表达式从三型文法向二型文法逼近。下面是我曾经写过的,用Perl来检查括号匹配的正则:


use re "eval";
#...  some other code
	my $ep=shift;
	my $notbrkt='[^()]*';
	my $brflag=0;
	my (@op,@result);
	while(1){
		my $flag=0;		#i need this flag to count matched bracket
		$ep=~m/
		($notbrkt)		#match things before the first bracket
		(			#match things inside the bracket
		(?:(?:
		\((?{++$flag})		#increase flag to counter matched bracket
		$notbrkt)+
		(?:
		\)(?{--$flag})		#decrease flag
		(?(?{$flag})		#see if flag is cleared
		$notbrkt)		#if yes, then there should be something behind
		)+
		)*)			#outer bracket and <blnkapp> finished
		(.*) 			#eat whatever at last
		/x;
		my ($front,$brkt,$back)=($1,$2,$3);
        #......  still something behind

        学过编译原理的人们应该知道,括号匹配是有状态的(括号嵌套的次数),需要通过变量或者栈来记忆当前所属的状态。因此通常的正则匹配是没办法记住这个状态的,从而使得括号匹配无法用传统语法的正则来实现。但是这个工作在Perl里实现也就这么简单。我这里的正则实现了这样的功能,$ep存放待匹配的字符串,通过正则匹配检查括号匹配情况,如果匹配,则把括号前、中、后三块赋值到$front, $brkt, $back中。其中括号匹配部分的正则是递归形式的。(这里提一下,传统正则是很难编写正确的递归形式的。由于传统正则没有条件匹配,也不能记忆状态,这使得递归的终结条件很难成功,要不然就一直匹配失败,要不然就让不正确的匹配成功。)

        这个正则匹配展示了Perl的正则表达式三个重要扩展:

  1. 变量展开。允许正则的部分规则存放在外部的变量里,并且在运行时展开,再进行正则编译;
  2. 代码内嵌。允许在正则匹配到某个部分的时候执行一段程序,在这里实现了状态的记忆;
  3. 条件匹配。当符合某种情况的时候才进行匹配或者不匹配。

        这三个功能让Perl的正则匹配更加强大,然而使用不当或许会降低可读性和可维护性,但是作为黑客开发时优秀的临时工具,这往往能极大地短缩开发时间。

       

       但是,上面给出的扩展性在其他语言或者框架上也不是完全没有被实现,就我所知.net也能玩这种玩意。但是Perl对正则匹配的处理速度估计是业界无人能匹的。对于一般的语言,如Java和Python,使用正则匹配的时候是通过OO的形式,需要运行时编译,然后才能通过方法来匹配。但是这在Perl是可选的:每一条正则都可以在解析时编译,或者运行时编译,这通过标识符/o来实现。而就算是运行时编译的正则,Perl从VM的架构上(Perl的运行时模拟CISC机器,好像包含300+条指令?执行正则匹配的是其中几条)对正则进行了优化,不要说其他语言,就算是grep指令也不一定及的上Perl。要比Perl更快,估计就只有手动编写的C程序了。这也是Perl被用作大量字符串工具的原因之一。


2. 函数式风格

        我说Perl借鉴了Lisp不是空穴来风。我似乎已经举不出比Perl的函数式特性更为强大的过程式语言。

        函数式的美感是每个程序员所羡慕的。但是很多语言,如C(编译式+硬编码),Java(缺乏lambda算子)等都不具有这个特性。而一些仿照函数式特性的语言也只是在原来的语言基础上换汤不换药,造成了一些基础的函数式特性,却不是真正的函数式。比如说C++的()重载允许了一定模板的函数动态生成,Python和C#具有lambda算子,但是一个就限制多多,一个就是仿出来的。

        什么是函数式风格?要让一门语言成为函数式的语言,最重要的门槛就是:函数是不是一等公民?而一等公民包含两个特权:

  • 函数应该允许被(运行时)传递
  • 函数应该允许被(运行时)声明、实现

        而在这么多种过程式语言中,只有Perl才能说是得到了很好的实现。

        函数式的风格的一大特点就是递归,下面是我曾经写过的一个函数,实现扫描给定文件夹下所有一般文件(包含子目录下的)并存放到数组的功能:

use File::Spec;

sub filter {
	my $routine=shift;
	my @ret=();

	map {push @ret,$_ if $routine->($_)} @_;
	return @ret;
}

#...

sub scanfiles{
	#...
		map {push @container,File::Spec->catfile($dir,$_)}
	                filter(sub{
				return -f File::Spec->catfile($dir,$_[0]);
			},readdir $dh);
	#...
}

        其中filter函数和python的filter函数实现同样的功能(对,Perl里没有filter函数,也没有sum函数,不能再郁闷...),scanfiles函数实现我上面所述的功能,我已经把代码裁剪至只有函数式的部分(这四行应该被当成只有一行)。这一部分实现的功能是把$dh描述的文件夹下的所有通常文件通过catfile处理后压入@container中。从这一句函数式的风格可以看出,函数式的代码更能保证自然人的思路顺序,并且让代码更加紧凑。可以看见,这里我创建了一个匿名函数,判断输进去的文件是否通常文件,并且我可以保证这个函数必然是运行时生成的。为什么这么说?注意到catfile函数的作用是把输入的数组按照当前OS的环境串成文件路径的格式,而我这串调用存在于一个foreach迭代块中,$dir是随着扫描目录一直在变的,如果是解析时生成,我的结果不可能是正确的。 

        这里表明了Perl的函数(正规术语是子例程)是运行时生成的!

        所以我完全可以写出下面的代码在主代码块,并且完全可以保证实际运行的正确性:

my @functions=();
map {push @functions,
                sub{ return $_ }
        } (1..$n);
#here $n is determined by any other logic and functions!!!!

        如果你看到这里,我相信你已经完全了解了函数式的特点,在这里我动态地生成了一系列的函数,这些函数返回1到$n,并且这里的上限$n根本不由也无须我这个代码块所决定。这里生成的代码块实现了当时LISP首次提出的流的概念。

        当然,在懒人眼里看到这个代码相信是另外一个想法:写C++的时候定义一个类的时候总要对private的数据成员手动编写get和set函数,多累啊!在Perl,你有福了:


my @attrs=qw(attr1 attr2 attr3);  #a lot and a lot

foreach (@attrs) {
        eval("sub $_ {
        my ($self, $val)=@_;
        $self->{$_}=$val if defined $val;
        $self->{$_}
}");
}

        这里的OO通过哈希来实现。这个代码块运行在OO风格的Perl的模块中,大意是一个懒人程序员通过foreach循环通过eval函数运行时生成了一批成员的get set函数(注意到Perl的get set函数是一体的,通过重载实现(ps. 尽管Perl的概念里不存在传统意义的重载(pps. 这一段括号嵌套可以用前面的括号匹配正则来检验哦)))。

        在Perl里,函数式是说不完的话题,我一开始先学习了Perl再学习了Scheme(一种Lisp的方言),发现所有的代码都可以用Perl来实现和解释。最后,以用Perl实现的函数式风格的OO来结束这一节:


package OOinPerl;

sub new{
        my $self=shift;
        my $type=ref $self || $self;
        #... somehow fetch the parameters from @_ and save in $a, $b and $c.
        my $data={a=>$a,b=>$b,c=>$c};  #this is a hash table reference
        my $this=sub{
                my $field=shift;
                $data->{$field}=shift if @_;
                $data->{$field}
        };
        bless $this, $type
}

#... other functions

        在这里,实例的本质是一个子例程,不要太神奇!

        (关于函数式风格的OO,通过Scheme也可以实现,感兴趣的人可以找我要代码,希望我们能够交流一下。)


3. 开发迅速

        把这个当优点可能有点牵强(倒不如说是上手迅速),这更多是从我自身出发来说的。

       我大概是一年多两年前才开始接触Perl,刚开始学习缺乏交流学习起来比较困难,后来慢慢地习惯了,通过看大小骆驼和黑豹书来学习,迅速熟悉过来,到现在已经是我解决小问题的主要语言(之一)。当处理起和文字相关的小问题的时候,Perl的代码量可以少得惊奇,往往都是 fetch=>filter=>handle三部曲就可以解决。在日常生活中,Perl往往就是编写一发写好解决问题再也不维护的小程序的语言。和一些标准模块结合,可以实现一些很实用的功能(比如说检索极影自上次检查时间为止,更新了哪些在追的动画,字幕组匹配)。而Perl对于这些任务,包括数据库的读取和回写,往往只需要200行不到的代码,对同等规模的代码,诸如Python等其他语言可能并不能完成同归模的问题。

        而对于有shell编程,sed、awk编程经验的人,掌握和使用Perl或许会更快。把所有的Linux脚本都用Perl来实现,说不准也是个相当有趣的做法。


4. 代码相当接近自然语言

        在这么多语言里面,也就数Perl是能用来编诗歌的。Perl的社区CPAN里面曾经有过Perl诗歌大赛,就是用Perl来编写诗歌。这样编写的诗差不多能够执行(可能要先定义一下额外的函数)。这得益于Perl自由的编程风格和各种语法糖使得Perl的代码可以很接近自然语言。通过读大骆驼可以知道很多让Perl代码更接近自然语言的方法。


5. 就因为酷 !

        扯了不少废话,其实我说Perl的特点一个字就可以概括,那就是:酷!

        Perl太酷了。对于每个开发者,针对同一个问题可能会有完全不同思路的解决方案,这一特性开拓了开发者的思路,应当受到开源开发者的追捧。想想一下,如果你有一个任务,目标是完成就可以了,并且涉及了大量的文字处理和网络相关的技术。你的同伴苦苦思索一步步解决网络,然后死在了大量而多变文字处理阶段,而你只要100+行就把问题完美解决了。当你把代码给你同伴看的时候,我觉得他心里的“what the fuck”感和你心里的痛快感可以形成鲜明的对比。

        Perl还有很多奇形怪状的特性支持着酷这个特点,什么上下文阿,字符串-数字平滑转换阿,语言定制阿都在其中。另外值得一提的是,Perl6的特性比Perl5更加酷,可以自由地定制自己的Perl,有更加动态的特性,只可惜因为解析器难以实现已经难产了十多年。不过最近有风声好像是快发布了(天晓得是真是假)。


三、Perl好像还真有这么些不好

        好吧,其实Perl现在在tiobe的排名上已经日益下降了,这不仅应该说时事上的因素和其他语言新增添的优势(Perl的语法和功能增添相对没这么频繁),Perl自身也有让人感到鸡肋的地方:

  1. 太酷了,酷到别人的代码基本看不懂。两个思路不一样的人又怎么可能在一起呢?想都想不到一块去,估计要光靠读代码读出个所以然是一件不容易的事情;
  2. 不好模块化。Perl和Python的模块化方式其实蛮像的。但是Perl在模块化编程的风格比较怪异,光靠读代码不读书很可能不能理解个中含义,因此有引出下一个缺陷;
  3. 可读性太差。这里的可读性差一在于变量的形式和正则表达式,二在于代码组织缺乏约束。Perl可以随处定义函数,也可以用各种方法生成函数。要是没有心理准备和先前知识,想去了解一段随手写的代码的行为,恐怕是一件不容易的事;
  4. 细节繁多。细节可以增加语言的威力,但同时也要求编写者对这门语言的各种细节了如指掌才能加以利用。比如说一开始给出Python的例子,如果不了解Python创造数组的时机,那么代码的行为和预想的不一样的原因也无从追起。同样,在Perl中,上下文机制,list和array这些概念让开始学习的我疑惑过一段日子,不过这些问题在Larry的大骆驼都能够能到语言学上的解释。


四、后话

        Perl是门很有趣的语言。真正投入学习过Perl的人估计都不会反对我。如果我提出的Perl的一些很爽很过瘾的地方吸引到了你,我真心奉劝一句:不妨学一学,Perl能打开一个全新的视角对待程序。


                
  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值