爲什麼我們要學/用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能打開一個全新的視角對待程序。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章