admin管理员组文章数量:1794759
为什么我们要学/用Perl?
今天发现我这个博客已经一个多月没有更新了,这个实在和初衷不符,另外项目压身,也是没有办法的事情,不过等这个项目做出来,或许还能写一篇日志留作后人用。
这篇日志是谈以Linux为开发环境下Perl的必要性,如果是在Windows下,可能Perl也就没有这么必要,而且在Windows下用Perl也有点违和感(不过我开始学Perl的时候倒是在Windows下给自己开发了一些工具,到现在依然常用)。所以从Windows下的开发者的角度来看,或许我这篇日志的视角就略显的老土了。
一、动机
我本人对Perl的感情是很真挚的,可能是仅次于vim的,碰到了很多问题(在Linux下),我第一直觉就是尝试用Perl来解决,而实际上往往都能在200行内用一个script解决,极大地节省了我的时间。而我在学习和使用Perl的过程中,未免感受到一些鸡肋,我觉得Perl的衰落是由多方面造成的:
但是在实践上,Perl的能力依然强大,我这篇日志也算是我对Perl重新壮大的一中希冀吧。
二、Perl和Python
Perl和Python算是一对冤家了,跟vim和Emacs一样,都互相斗了很多年。但和Emacs不一样,Python我还是用的。但作为一篇赞颂Perl的日志,我还是免不了俗,得数落一下Python:
- 长得越来越像其他流行的语言;
- 更新缓慢;
- 每次更新的同时会增添新特性,且基本维持所有原有特性,只对少数实验性、不合理的特性作删减。
所有的这些特点都是为了减低开发者的学习成本,并且保持已有代码的可维护性。但是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的正则表达式三个重要扩展:
这三个功能让Perl的正则匹配更加强大,然而使用不当或许会降低可读性和可维护性,但是作为黑客开发时优秀的临时工具,这往往能极大地短缩开发时间。
但是,上面给出的扩展性在其他语言或者框架上也不是完全没有被实现,就我所知也能玩这种玩意。但是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自身也有让人感到鸡肋的地方:
四、后话
Perl是门很有趣的语言。真正投入学习过Perl的人估计都不会反对我。如果我提出的Perl的一些很爽很过瘾的地方吸引到了你,我真心奉劝一句:不妨学一学,Perl能打开一个全新的视角对待程序。
版权声明:本文标题:为什么我们要学用Perl? 内容由林淑君副主任自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.xiehuijuan.com/baike/1686533279a78759.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论