最近在看Greg London 的Impatient Perl,再次感受Perl 的奇怪(不过之前用过Ruby,有些东西也见怪不怪了)。
都说Perl 是以实用为第一设计准则的,这是否就是说它简单,限制很少,同时并不漂亮(统一的形式,更少的语法,比如Lisp),因为在现实中千奇百怪的需求面前它选择了妥协(折中更好听些)?
都说Perl 代码很丑(写一遍就扔掉,后面很难看懂(包括本人)),是否是因为Perl 本身松懒的语法养成了Perl 程序员的随性?
都说Perl 有很多奇怪的预设变量,提高了初学者的门槛,但在我看来这只是Perl 众多奇特风俗中的一个。
说明:
我憎恨早期翻译计算机领域那帮人的随意,耶稣会引入God, Lord,gratia 之类的词时还专门挑选了一些汉语里最接近的词(天主、上帝、圣恩)来表达呢,你们就随随便便地把hash 译作哈希,array 叫做数组,Object Oriented 称为面向对象……所以,下面我采用自己的翻译(你可能不习惯,但这正如Perl 有自己的思维习惯):
array,序列(突出它是有序的,而且对存储的元素没有限制);
list,序列(在我看来,list 就是不能修改的序列???);
hash,字典(其实也想过用映射表的,但字典更通俗一些。关联序列的叫法没说出它无序的性质);
lexical variable,局部变量(总觉得没表达出它是被限制在lexical scope 中的意思);
Object Oriented,物件导向(台湾的译法,的确,OO就是围绕Object 和其间的关系来设计程序的);
前导符
拿命名来说吧,大多数语言虽然都提倡给变量取个好名字,但一般都没什么限制,Perl 就非得搞个前导符(sigil):
看似Perl 还算严谨,名字上区分都这么严格,那么请看下面这个例子:
看到了吧,同一个名字加不同的前导符会被Perl 解释成不同的数据结构(当然这种做法是不提倡的啦,但你也可以由此看到Perl 内部的某种统一)。
my
Perl 程序中声明变量一般喜欢在前头加上my,作用有点像把变量限定为局部的(正式名称叫lexical variable)。一旦出了所在的词法作用域(lexical scope),加了关键字my 的变量就不再有效。那你干嘛不写local,弄个什么my,你咋不弄个your呢。你还真别说,your是没有,our倒有一个。被声明为our 的变量只能被它所在的名字空间(namespace)直接引用(main 空间里的除外),别的名字空间要想引用必须写全称,即名字空间::变量名。回到my 的话题上,局部变量不属于任何名字空间,出了所在的块就不能再被访问,除非它有个还没被销毁的引用。
序列和字典
我认为,一门语言越接近人类语言,它对歧义的包容和推理能力越强,即在不同的语境下,同一句话能传达不同的意思。Perl 的序列在不同的语境(context) 下就有不同的含义。
Perl 的序列定义时用@ 前导符,要访问序列中的元素时却使用$ 前导符加[索引],让人很是不解(难道是为了强调取得的是个标量?!)。Perl 的字典类似,但不用方括号,而用花括号(像Ruby 那样统统方括号不好吗)
最末一个元素的索引可以用“$#序列名”取到,据说这一丑陋的语法来自C shell。我取最末元素的下标干啥,想知道序列的容量还得加个1,多麻烦,而且你还提供了另一种更简洁的方式来访问最末元素——负数索引:$ary[$#ary] 等价于 $ary[-1]。
Perl 把要存储的数据放在圆括号中,用逗号隔开(你也可以不用逗号,改用“肥逗号”,即“=>”。不要惊奇,Perl 的世界里一切皆有可能)。如果你懒得打逗号,也可以把东西放进qw()(quoted words 或者quoted by whitespace 的简写)里,用空格隔开就行。不喜欢圆括号也没关系,可以随便换成任何成对的符号。现在看着很方便吧,一会你就不会这么想了,qw 的七姑八姨(q, qq, qx, qr)马上就来,一定让你满眼金光。
Perl 口口声声说序列只能包含标量,不能包含其他序列,那这程序也没见错啊:
只是Perl 会把传入的序列抹平。要想真正实现嵌入,得加上方括号:
更诡异的是,用$lst[0] 只能得到类似“ARRAY(0x10086cfc8)”的东西,要想取得值必须这么干:
@{$lst[0]}
Perl 用$_ 作缺省变量,这点无可厚非,Python, Mathematica 不都习惯用‘_’吗。
bless()
前面的要说奇怪,顶多也就是另一种约定俗成,Perl 里的Object Oriented 才叫一绝。Perl 中没有class或是类似的关键字,它用模块来模拟类(说是模拟,因为它缺少类的很多功能,比如数据的私有???)
Perl 的每个包(package)对应一个.pm (perl module)文件,且包的名字和.pm 文件的名字一致(一般自定义的包名各单词首字母大写,内置的包名一般用全小写)。如果一个文件里放了多个包,use 的时候会找不到对应的文件。Perl 还要求所有的模块以“1;” 结尾,否则会出错。
模块就模块嘛,非得叫什么包,还要求虚拟的模块与实体的文件一一对应。这还不说,还要用“1;” 来表示模块结束,表明返回了一个真值-_-!!
声明了包,你就可以用包名::函数名(限定包名)的方式调用模块中定义的函数了(有点像类函数???)
下面该介绍Perl 里物件导向机制的主角—— bless() 。其实bless() 本身很简单,仅仅是篡改ref() 的返回值。
还记得ref()吗?它能判断出传入值的类型:ARRAY, SCALAR, HASH, CODE, 或是空串(表示未知类型或传入的不是一个引用),功能上有点像Ruby 里的class() 函数。
而bless() 篡改的恰恰是(也只影响)ref() 的返回值,即间接地修改了传入值的类型(说你是你就是,不是也是^_^)。
说到这,你可以隐隐约约看到bless() 和Perl 的物件导向编程间千丝万缕的关系了。可能你会奇怪,好名字多的是,为什么叫bless?Perl 老爹的思维是这样的,被祈祷过的(blessed) 水虽然和普通水别无二致,却会被称为“圣水”,只是变了个名字,其它什么都没变。看到了吧,bless() 改变的仅仅是引用的名字,并没有改变引用的内容。但仅仅是这点差别,却可能产生行为上的差异。
前面不是说可以通过限定包名的方式调用函数,还可以等价地写成“调用者 -> 函数名”的形式。
Perl 用use base 语句实现继承,而“use base 基类名”其实是将基类push 进@ISA (一个包含了当前包所有基包的序列,ISA 就是is a)。
现在似乎一切都有了,那为什么说bless() 是Perl 物件导向机制的基础呢?有了bless() 你就可以指鹿为马了:
use Animal;
my $inv = bless {}, "Animal";
$inv->Speak(2);
那又怎样?如果你把它放到函数里面呢?
New() 可以这样简写:
在我看来,Perl 有一套复杂而特别的习俗。它总的原则就是少敲字,所以不断地在语言里加入各种记号,语言的风格也鼓励程序员运用这些短小、怪异的记号。这种风俗的结果就好像没有规划(或者前期有规划,建造完成后无管理)的下水道系统,需要时临时挖一条,遇到旧管道就绕过(绕不过就可以抱怨了,“当时怎么规划的!”),时间久了,整个语言就变成了一个无法理解的庞然大物(从Perl 6 的开发可见一斑)。对我而言,这些捷径就像结绳记事一样,久而久之,自己都搞不清为什么结绳了,这时才肉牛满面——还不如当初多写几句呢-_-!
P.S. 译文《What's wrong with Perl》
这是Lars Marius Garshol 2002年1月写的一篇文章,鉴于近十年来Perl 没有太大的变化,我决定翻译这篇老文。
_______________________________________________________________________________
本文所谈仅仅是我个人对Perl 的理解。我欢迎大家提出自己的看法。我看到很多人在Usenet 上发问,怎么学习Perl,在这,我只想陈述自己的看法——我会告诫他们不要学习Perl。
如果你认为本文有什么明显的错误,请与我邮件联系。如果你只是不同意,也请告诉我。我会尽可能纠正文中的错误。
可能还需要解释为什么我把Perl 称为骆驼(the Camel)。Perl 之父Larry Walls 写过一本Perl 全书《Programming Perl》,O’Relly 出版社发行这本书的时候用一只骆驼作为封面。从此,这本书也被叫做“骆驼书”,而Larry Wall 也经常用骆驼来指代Perl。
本文使用Python 1.5.2和Perl 5.005,如果以后的版本有什么新功能,请告之。
我是在97年初开始学习Perl 5的。我下了Patrick M. Ryans 写的一篇很好的Perl 简介,感觉很多用C, Pascal, Java 很难处理的问题在Perl 里迎刃而解。Perl 在文本处理和系统交互方面如有神助。我还读了Randal Schwartz 的“Learning Perl”(也被称为羊驼书(llama book))。
我快速翻阅了这本书,发现了一些有意思的新功能。我仅仅花了半个小时就完成了我的第一个程序,用来读web 服务器的日志,然后统计各页的访问次数。它不仅运行得很好,而且似乎能一些忽略一些不重要的错误,要是用C/Pascal/Java 来写恐怕早就崩了。
(这个程序更新了好几版,不少人在用。可见,Perl 不是不够强大,而是不够方便)
我曾经痴迷这门语言。随着了解的深入,我渐渐发现一些不合口味的东西。我把这些东西列成表,现在,这表已经很长了。
我承认牺牲了一点可读性……
——Larry Wall
我开始厌恶Perl 最初是因为它的语法。Perl 是门复杂的语言,有很多操作符和特殊的语法。这就意味着,代码越长,隐含的语法错误就越多,而且复杂语法也给读代码造成了更多的困难。这样,理解或者维护别人的代码就变得更难了。
我追求简洁漂亮的代码,对那些容易造成混淆的特性敬而远之,但即便如此,它还是很难读懂。下面就是一个常见的例子:
这还不算最糟的,这有个产生Soundex (译注:一种语音算法,利用英文单词的读音计算近似值,值由四个字符构成,第一个为字母,后三个为数字)值的Perl 函数:
这还不是最难读的。Perl Journal 杂志每年会举办一届最糟Perl 代码大赛。 获胜者的代码要么是单词读不懂,要么是意思猜不透(这不能说明Perl 可读性不强,只是有太多鬼灵精怪的东西罢了)
有人读这篇笔记的时候会说“不管什么语言,总有人能写出狗屎一般的代码!”。确实,但也要看到有些语言似乎倡导晦涩难懂的风格,有些则倡导简单明快。
从我自身和其他一些人的代码来看,Perl 属于前者。在Perl 代码堆里,很多脚本都很短以至于不难读懂,但你随便翻翻还是能每两三页就看到一个像前面的Soundex 算法那样的例子。
有些人争辩说Perl 比其他大多数语言都更接近自然语言,至少对我来说这话不假。但自然语言不仅异常复杂、含混不清,而且有大量的词义差别很小的近义词。我可不想我的程序变成那个样子,但它有这方面的趋势了。你是那一类呢?
Perl 把很多特性直接写进内核,而不是作为独立的库来调用。正则表达式就是个例子。Perl 的正则表达式有自己的语法,这样在处理常用事务时很方便,但也意味着你不能借用物件导向??的优势。
Perl 有个叫format 的特殊构件??,用这玩意你可以生成漂亮的文本报告。format 很好使,但是被做进语言里了,所以,你不能创建format 序列,把它们作为函数的返回值……很多情况下它都不够方便。
你可以操作文件,但因为他们被内建到语言中,所以我从没搞懂它们是怎么工作的???。我曾经尝试使用引用,但从没成功过。
Perl 文档里用了好几页来演示如何序列和字典多重嵌套的数据结构。我反反复复读了好几遍,费了老鼻子劲才搞清楚工作原理。我发现了一些很奇怪的东西,如果是用其他语言完全不用考虑这些问题。
在Lisp 里,这样来赋给变量a 一个序列:
其任意元素(以第一个为例)可以为另一序列:
如果用Perl,第一个序列可以这样表示:
这是第二个:
(变量前的@ 符说明这些变量是序列)目前为止还行,接下来我们试试访问元素。
在Lisp 里面,访问第一个元素要这样写:
Lisp 返回
1
同样地,第二个序列b 的第一个元素:
Lisp 返回
现在试试Perl:
返回
1
变量名前的$ 是告诉Perl 我们想要一个单值(Perl 术语叫scalar),而不是一个序列。[0] 标明我们想要这个序列的第一个元素的值。就像其他大多数语言和API 接口一样,Perl 从0开始计数。
我们试试
Perl 很高兴地给我们返回了
0.8
没错,Perl 会把嵌套的序列抹平,也就是说序列b 现在有6个地位平等的(译注:原文是consecutive,我觉得作者想说的是:前三个元素并不属于嵌套在序列中的序列)元素。
要想获得嵌套序列,我们要这么写
注意方括号的作用是:在外层序列第一个元素的位置上置一个引用,这个引用指向内层序列。
(匿名序列!!!!)
现在我们再试试:
$b[0]
返回
ARRAY(0xb75eb0)
现在我们想要的是元素的内容。问题出在$ 上,Perl 认为我们想要一个单值,所以返给我们一个序列的引用,而不是序列本身(序列不是单值)。
这样看来我们得用
@b[0]
@告诉Perl 我们想要的是序列的内容。试一下,
ARRAY(0xb75eb0)
丫的,还是这个。这一刻,我没去想这是为什么,而是放弃了。
几周以后,我看到一个帖子,给了我一线希望:在访问序列中引用的内容时,要这样写:
@{$b[0]}
终于拿到了
(0.8 0.9 1)
现在,我可以用上嵌套序列或是嵌套字典了。(译注:想起我的本科毕业设计,需要一个很复杂的数据结构,序列、字典一炖乱套,现在想来真是庆幸用的是Ruby ^_^)
现在,回想一下:你真的需要嵌套序列吗?
Perl 的另一个显著缺陷就是缺少函数签名(函数,Perl 里叫子程序(subroutine))。大多数语言都提供了函数签名,把参数的名字(有些甚至附上类型)罗列出来。但Perl 没这么做。
例如,这样一段Java 代码
用Perl 改写就成了这样
sub substring {
local($str, $from, $to) = @_;
换言之,你手动解析参数序列。Perl 最近加入了原型记号(the notion of prototypes),所以你可以这么写
sub substring($, $, $) {
local($str, $from, $to) = @_;
这样,Perl 会检查参数的个数对不对。但这不是强制的,事实上很多Perl 代码都没这么写。
事情还没有结束,很多程序员都不会像上面那样去解析参数,使得代码更难读了,而要自动生产文档似乎也变得不可能。
进而别的高级语言中的那些特性,比如参数指定(keyword arguments),你也用不了了(当然你也可以用字典自己实现)。例如,如果你在Common Lisp 中用一个叫make-hash-table 的函数来创建字典,这个函数带以下参数:
下面这些创建字典的方式都没错:
(make-hash-table)
(make-hash-table :test #'eq)
(make-hash-table :size 1000)
(make-hash-table :rehash-size 2.0 :rehash-threshold 0.7)
如果你用到的函数参数很多,这种特性就很有用,既方便又清晰。你也可以在Perl 里这么干,但是不鼓励,自动生成文档也不方便,代码既不容易读懂,肯定也不如Common Lisp 中那么方便:
(defun make-hash-table(&key test size rehash-size rehash-threshold)
虽然物件导向不如很多人相信的那么神奇,虽然Perl 也支持,但只是半路出家,可以说是在这门语言生命的后期(???)才被添加进来。
如此一来,普通文件、套接字(socket)、字典和序列都不是原生物件,就意味着它们支持的接口本应该更方便的。Perl 的新版本把这些东西打包成物件导向风格的模块,这样只要遵从Perl 提供的协议,你就能自己去实现这些协议。但这样一来,你就不大能分清是普通的file handle,还是file object了。
还有就是,当你创建一个物件时,需要自己去管理物件内部的构件。在Perl 中,需要手动创建物件。类(class)被声明为包(package),包中的函数就成了类的方法。你需要一个映射表作为物件的原材料,然后篡改它的引用的类型(用内置函数bless())。手册里perlobj 那部分具体解释了Perl 的物件特性,推荐用下面的模版来初始物件:
package MyClass;
sub new {
my $class = shift;
my $self = {};
bless $self, $class
$self->initialize(); # do initialization here
return $self;
}
对于初识物件,还有其他方法,但可能会出现继承方面的问题。个人觉得,用这种方式实现物件导向真是不可思议。同样的东西,用Python 来写就很简单:
class MyClass:
pass
如此笨拙的定义方式让你很难察觉某块代码实际上是在定义类,也很容易导致物件创建错误。
总的说来,Perl 是门宽泛而复杂的语言,需要花很长时间来学习。在我看来这种复杂性是不必须的,一门简单的语言或许能取到更好的效果。我想这也意味着许多一般水平的Perl 开发者写着次优的代码。
只有很少的Perl 开发者能写成普适的可重用模块,因为你必须非常了解这么语言,很多东西需要花费大量的时间来研习,但这门语言本身却不鼓励花费如此之多的时间来学习如何写出这样的代码(译注:Perl 是门务实的语言,需要啥补啥)。
(译注:后面是说作者如何转向Python 的,有点偏离本文的主题,就不译了^_^)
……