PHP黑系列之二:PHP 為什麼函數命名是如此不一致?
PHP 的函數命名的不一致是被很多人詬病的,如 Problems with PHP 寫到:
No consistent naming convention is used. Some functions are verb_noun() and others are noun_verb(). Some are underscore_separated, while others are CamelCase or runtogether. Some are prefixed_byModuleName, and others use a module_suffix_scheme. Some use "to" and others use "2". And if you take a random set of ten library functions, chances are half a dozen different conventions will be included.
對此,在「PHP 是最好的語言」這個梗是怎麼來的?這個回答中,@濤吳 寫道:
……告訴你一個簡單的例子來說明 PHP 有多匪夷所思:PHP 的核心函數命名很不一致,有 「strptime」這樣類 C 函數的名字,有「nl2br」這樣的簡寫,卻也有「htmlspecialchars」這樣的長名。後來人們發現這種不一致並非偶然,而是當 PHP 還是只有不到一百個函數的小語言的時候,其作者決定用函數名的字元數量——來作為函數的 hash(!)。由於這個愚不可及的決定,PHP 的函數名長度要儘可能地長短有秩、均勻分布,影響一直延續至今。
乍一看,只覺得匪夷所思,不過作者給了出處。用函數名的字元長度作為hash這個說法來自於 http://news.php.net/php.internals/70691,PHP 的創造者 Rasmus 寫道:
Well, there were other factors in play there. htmlspecialchars was a very early function. Back when PHP had less than 100 functions and the function hashing mechanism was strlen(). In order to get a nice hash distribution of function names across the various function name lengths names were picked specifically to make them fit into a specific length bucket. This was circa late 1994 when PHP was a tool just for my own personal use and I wasnt too worried about not being able to remember the few function names.
-Rasmus
一開始,我覺得是不是 Rasmus 是開玩笑的?甚至如果不知道這是 Rasmus 本人寫的,我都懷疑是不是來「釣魚」的。
所以我特意查看了PHP的老代碼。在 PHP 1.99s 和 2.0.1 的代碼里確實可以看到,lex.c 文件(題圖即為該文件相關源碼)里先是創建了一個 cmd_table ,如下:
static cmd_table_t cmd_table[PHP_MAX_CMD_LEN+1][PHP_MAX_CMD_NUM+1] = {n { { NULL,0,NULL } }, /* 0 */nnn { { NULL,0,NULL } }, /* 1 */nnn { { "if", IF, NULL }, /* 2 */n { NULL,0,NULL } }, nnn { { "max", INTFUNC1,ArrayMax }, /* 3 */n { "min", INTFUNC1,ArrayMin },n { "key", KEY,NULL },n { "end", END,NULL },n ...n { "shr", INTFUNC2,shr },n { NULL,0,NULL } }, nnn { { "echo",PHPECHO,NULL }, /* 4 */n { "else",ELSE,NULL },n { "case",CASE,NULL },n ...n { "ceil",INTFUNC1,Ceil },n { NULL,0,NULL } }, nn ...nn};n
然後在 lexical analyzer 的代碼里根據 token 查找指令的函數是這樣定義的:
/* Look up a command in the command hash table n * If not found, assume it is a user-defined function and return CUSTOMFUNCn */nint CommandLookup(int cmdlen, YYSTYPE *lvalp) {n register int i=0;nnn if(cmdlen<=PHP_MAX_CMD_LEN) while(cmd_table[cmdlen][i].cmd) {n if(!strncasecmp(&inbuf[tokenmarker],cmd_table[cmdlen][i].cmd,cmdlen)) {n *lvalp = (YYSTYPE) MakeToken(&inbuf[tokenmarker],cmdlen);n LastToken = cmd_table[cmdlen][i].token;n return(cmd_table[cmdlen][i].token);n } n i++;n }n *lvalp = (YYSTYPE) MakeToken(&inbuf[tokenmarker],cmdlen);n return(CUSTOMFUNC);n}n
而執行內建函數的定義是:
void IntFunc(char *fnc_name) {n int i=0;n int cmdlen = strlen(fnc_name);nnn while(cmd_table[cmdlen][i].cmd) {n if(!strncasecmp(fnc_name,cmd_table[cmdlen][i].cmd,cmdlen)) {n cmd_table[cmdlen][i].fnc();n break;n } n i++;n }n}n
所以,代碼證明,確實 PHP 的早期版本就是拿 strlen(fnc_name) 作為 hash 的 ?? 。
這則 post 在 reddit 上也不出意外的引發了許多討論。有人表示這是Rasmus當年做的trade-off,然後遭到許多人的質疑,因為必須得有(相對於缺點的)優點,才談得上是trade-off,但是拿函數名長度做hash看不出有任何好處。
實在要找「優點」的話,或許只能說是:能非常直觀的看到hash table的bucket數量和每個bucket里有哪些名字,非常容易手動維護——比如可以手動調整每個bucket里名字的順序(通過迎合token出現的概率)來略微提高一丟丟性能。
不過直接用首字母作為hash其實也差不多擁有同樣的優點。而且還有更大的優點,你完全不用故意均勻分布,如果某個字母下數量太多,直接再按下一個字母hash就好了——好吧,這實際上是退化的tree,你可以在必要的時候在某個分支上進化一下。
其實,從軟體工程的角度說,合理的方式是,一開始實現為直接遍歷,然後在將來(必要的時候)進行性能優化——使用一個成熟可靠有人維護的 hash table 庫。
總之,這件事情上要為 Rasmus 開脫,是非常困難的。
講到這裡,好像已經沒啥好多說(黑)的了。
不過,這畢竟是非常早的 PHP 版本,至晚到 PHP3,這個部分已經完全重寫了。所以按道理說,這一「愚不可及的決定」對函數命名造成的影響,本不應「延續至今」。
實際上,還有另一個也許更重要的原因。也是出自於 Rasmus:You also need to realize that there is consistency. It is just consistency from a different angle. PHP from day one was always a very thin wrapper on top of dozens, now hundreds, of underlying libraries. The function names and argument order, for the most part, were taken directly from these underlying libraries. So if you were familiar with MySQLs C API, for example, you would instantly be able to navigate PHPs mysql functions to the point where we barely needed PHP MySQL documentation because MySQLs C library documentation covered it function for function. And for many of the str functions (the ones without an underscore), try typing: man strlen/strchr/strrchr/strtok/strpbr/strspn... at your Linux command line prompt.
This approach covers the majority of the functions in PHP. The others are somewhat haphazard because it was not always obvious how to name these given there was no underlying API to mimic.
簡言之,這無非是把鍋甩給 C (和用 C 寫的庫和 API)。
但是呢,必須說這個鍋甩的有點勉強,畢竟其他編程語言,即使是那些著名的膠水語言如 python,也很少直接在核心庫照搬 C 的 API。
至於最後一句,那意思是「沒有底層API時我們乾脆就亂來了」!對此,只好呵呵了。如果結合早期的hash問題,我覺得這也許是破窗效應的「友善」說法吧……
PS. 破窗效應或許也可以描述 PHP 粉絲社區的問題。
最後,PHP 社區自身不是沒有改善函數命名一致性的動議,如 rfc:consistent_function_names ,但貌似沒有什麼進展,也許這是註定徒勞無功的?
本篇完。
推薦閱讀: