如何評估比較程式語言

一月 8th, 2010 由 victor Leave a reply »

有人提到以過去的觀點來看現在的PHP是不公平的,確實我有好一陣子沒有碰PHP,對於新版的PHP並不瞭解,因此我花了一些時間檢視新的PHP規格,的確新版的PHP遠比我預料的進步許多,很多之前提到的問題有所改善,我承認上一篇文章說PHP很爛是在發洩過去對於PHP的種種不滿,有人認為程式語言沒有好壞,全部都事在人為,而我認為程式語言的好壞有主觀也有客觀的部份,當你討厭或喜歡一個程式語言一定有某些原因,但就客觀的來看語言的設計來講,PHP在早期真的是爛得一榻糊塗,是公認的爛,但是在當時少有選擇,能選的工具不多,就只有PHP,在累積夠久的不滿後就會一次暴發開來,對於現在的PHP5.3還有未來的PHP6而言,他已經跳離了以前的單純語言設計上的爛,往更好的方向前進,我能說PHP的壞話少了很多,因為當語言層面的問題大部份都解決了,接著就是哲學和個人喜好的問題了,對與目前和新版的PHP的誤解在這邊說聲抱歉

所以這篇我想說的,怎樣看客觀地評估一款語言的好壞,還有主觀地評估好與壞,很多人說程式語言沒有好與壞,只有適合和不適合,基本上我不認同這樣的說法,如果說兩種差別很大用途不同的語言拿來一起比較就很奇怪,但是如果是性質接近的語言都符合你需求,就可以做好壞的比較,不然你要怎麼做選擇? 就如同我們買車子好了,你的目的是載貨,買的自然是卡車、貨車,但是如果是轎車呢? 就算是卡車也有選擇的,程式語言也一樣,目前大部份人所見到的程式語言,都是所謂的』通用目的』語言,也就是說這樣的語言沒有預設任何用途,基本上他可以拿來做任何用途,很多人因為不知道要從何比較起,所以都說程式語言沒有好壞之分,只有適合和不適合,如果有一堆工具都適合,既然沒有好壞之分,那你又該如何做出進一步的評估與選擇? 適不適合不就是透過比較而來的嗎? 不比較怎麼知道適不適合? 這不是自相矛盾嗎? 只靠喜好嗎? 閉著眼睛亂選?  大家都知道車子我們可以比較它的馬力、耗油、安全評等,但是大多數人不知道程式語言該比較些什麼,而我今天所要說的,就是程式語言該拿什麼來比較

客觀的評估條件

首先,客觀的評估條件並不是完全客觀的,在某些情況它會帶有主觀色彩,例如一個語法的可讀性如何,這就帶有某種程度的主觀,事實上有一個領域是在探討這些問題的,在資訊工程系開的一門課叫「程式語言」就有某部份在討論這些,但是對於這部份沒有很深入,而關於這些條件應該有更嚴僅學術上的方式來對這些條件進行評估,但是那超出我的知識範圍,我就我所知道的來說

可讀性

可讀性是指一個程式語言寫出來的語句是否容易閱讀

可讀性是程式語言很重要的一個優劣的參考指標,因為有一個事實就是

程式被讀比被寫還多次

一個程式的可讀性,關係到維護的人能否輕易的瞭解程式語言所表達程式的意圖,如果維護的人難以理解某段程式所要表達的事情,那麼這些程式就難以被維護,軟體工程中有個很重要的概念就是,程式開發的成本其實只佔一小部份,而維護其實所花的成本會比開發來得多,在這樣的情況下,如果寫出來的程式難以被讀懂,那麼接手的人將難以進行維護,如此一來可能要面臨整個程式重寫的情況,會使得維護的成本大大增加,程式碼也失去了重覆利用的價值,所以可讀性對於一款程式語言的好壞來說是很重要的一個指標

為了說明可讀性爛的程式語言,我找來最極端的例子,Brainfuck是一款以圖靈機為模型,所做出來最小化的程式語言,它所寫出來的Helloworld程式如下

++++++++++[>+++++++>++++++++++>+++>+<<<<-]
>++.>+.+++++++..+++.>++.<<+++++++++++++++.
>.+++.——.——–.>+.>.

我想應該沒有太多人會反對我說Brainfuck可讀性很差 XD

我喜歡極端的例子,再來一個國際混亂碼大賽的參賽作品,這是一個要將程式寫到別人看不懂,但又要能夠運作,而且要夠創意的比賽,所以寫出來的程式都是可讀性0分的程式,下面這個是可以計算圓周率的程式

 #define _ -F&lt;00||--F-OO--;
 int F=00,OO=00;main(){F_OO();printf("%1.3f\n",4.*-F/OO/OO);}F_OO()
 {
             _-_-_-_
        _-_-_-_-_-_-_-_-_
     _-_-_-_-_-_-_-_-_-_-_-_
   _-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
 _-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
  _-_-_-_-_-_-_-_-_-_-_-_-_-_-_
   _-_-_-_-_-_-_-_-_-_-_-_-_-_
     _-_-_-_-_-_-_-_-_-_-_-_
         _-_-_-_-_-_-_-_
             _-_-_-_
 }

當然,這是故意寫出來難懂的例子,有些程式語言天生寫出來的程式就較難懂,或是較容易寫出難懂的程式,舉個例子,Perl程式可讀性和Python比較起來就差很多,下面兩隻程式的目的都一樣,是一隻簡單的Web Proxy程式,Python版本是我參照Perl版寫出來的,特意按照同樣的架構下去寫,目的就是要能容易做比較

Perl版:

#!/usr/bin/perl
#-------------------------------------------------------------------------
#--     proxy.pl    -  A simple http proxy server.                      --
#
#	The original program was from:
#		http://www.cis.upenn.edu/sdt/proxy.pl
#
#	The original program employs the basic socket library, which
#		seems hard to understand for some beginners.
#	That is why I modify it  by employing the
#		IO::Socket::INET
#
#	jay.s.liu@gmail.com
#	Apr. 17, 2006
#--                                                                     --
#--     To run, type   proxy.pl [port-number]   at the shell prompt.    --
#--     Default port number is 5364.                                    --
#--                                                                     --
#-------------------------------------------------------------------------
use IO::Socket::INET;
srand (time||$$);
 
#---  Define a friendly exit handler
$SIG{'KILL'} = $SIG{QUIT} = $SIG{INT} = 'exit_handler';
sub exit_handler {
    print "\n\n --- Proxy server is dying ...\n\n";
    close($SOCKET);
    exit;
}
 
$| = 1;	#Disable buffering
 
##--- Step 1 : Create Socket ---##	
 
#--- Get the port number form commemd line
$proxy_port = shift(@ARGV);
$proxy_port = 5364 unless $proxy_port =~ /\d+/;		
 
#--- Create a socket that listens to a specific port
&amp;listen_to_port($SOCKET,$proxy_port);			
 
$local_host = `hostname`;
chop($local_host);
$local_host_ip = $SOCKET-&gt;sockhost();
print " --- Proxy server running on $local_host( $local_host_ip ) port: $proxy_port \n\n";
 
#---  Loop forever taking requests as they come
while (1) {
 
##--- Step 2 : Wait for request ---##
    print " --- Waiting to be of service ...\n";
    $CHILD = $SOCKET-&gt;accept() || die "accept $!";
 
#--- Get the ip address of clinet
    $inetaddr = $CHILD-&gt;peerhost();
    $port = $CHILD-&gt;peerport();
	print "Connection from ", $inetaddr, "  port: $port \n";
 
#---  Fork a subprocess to handle request.
#---  Parent proces continues listening.
    if (fork) {
        wait;           # For now we wait for the child to finish
        next;           # We wait so that printouts don't mix
    }
 
##---  Step 3 : Read first line of request and analyze it. ---##
#---  Return and edited version of the first line and the request method.
  ($first,$method,$URL) = &amp;analyze_request;
 
##---  Step 4 : Send request to remote host ---##
    print $URL $first;
    print $first;
 
#-- Read HTTP header and Send it to remote host
    $contentLength = 0;
    while (&lt;$CHILD&gt;) {
        print $_;
        next if (/Proxy-Connection:/);	#discard the line which begins with the string "/Proxy-Connection:/"
	if (/Content-length:(.*)/i) {
	    $contentLength = $1;
	}
        print $URL $_;
        last if ($_ =~ /^[\s\x00]*$/);	#exit loop if the line only contains space and null character
    }
 
#Read HTTP content and Send it to remote host
    if ($method eq "POST") {
		if ($contentLength != 0) {
		    read $CHILD, $data, $contentLength;
		}
		else {
		    $data = &lt;$CHILD&gt;;
		}
        print "POST body: $data";
        print $URL $data;
    }
    print $URL "\n";
 
##---  Step 5 : Wait for response and transfer it to requestor. ---##
    print " --- Done sending. Response: \n\n";
    $header = 1;
    $text = 0;
    while (&lt;$URL&gt;) {
        print $CHILD $_;
        if ($header || $text) {      # Only print header &amp; text lines to STDOUT
            print $_;
            if ($header &amp;&amp; $_ =~ /^[\s\x00]*$/) {
                $header = 0;
            }
#           if ($header &amp;&amp; $_ =~ /^Content-type: text/) {
#               $text = 1;
#           }
        }
    }
    close($URL);
    close($CHILD);
    exit;                       # Exit from child process
}
#-------------------------------------------------------------------------
#--     analyze_request                                                 --
#--                                                                     --
#--     Analyze a new request.  First read in first line of request.    --
#--     Read URL from it, process URL and open connection.              --
#--     Return an edited version of the first line and the request      --
#--     method.                                                         --
#-------------------------------------------------------------------------
sub analyze_request {
#---  Read first line of HTTP request
    my $first = &lt;$CHILD&gt;;
    my $url = ($first =~ m|(http://\S+)|)[0];
    print "Request for URL:  $url \n";
 
#---  Check if first line is of the form GET http://host-name ...
    my ($method, $remote_host, $remote_port) =
        ($first =~ m!(GET|POST|HEAD) http://([^/:]+):?(\d*)! );
 
#---  If not, bad request.
    if (!$remote_host) {
        print $first;
        while (&lt;$CHILD&gt;) {
            print $_;
            last if ($_ =~ /^[\s\x00]*$/);
        }
        print "Invalid HTTP request from ",$inetaddr, "\n";
        print $CHILD "I don't understand your request.\n";
        close($CHILD);
        exit;
    }
 
#---  If requested URL is the proxy server then ignore request
	my $local_host = `hostname`;
	chop($local_host);
	my $local_ip = (gethostbyname($local_host))[4];		#Get localhost ip (packed imformation)
    my $remote_ip = (gethostbyname($remote_host))[4];		#Get remote ip (packed imformation)
    if (($remote_ip eq $local_ip) &amp;&amp; ($remote_port eq $proxy_port)) {
        print $first;
        while (&lt;$CHILD&gt;) {
            print $_;
            last if ($_ =~ /^[\s\x00]*$/);
        }
        print " --- Connection to proxy server ignored.\n";
        print $CHILD "It's not nice to make me loop on myself!.\n";
        close($CHILD);
        exit;
    }
 
#---  Setup connection to target host and send request
    $remote_port = "http" unless ($remote_port);
    &amp;open_connection($URL, $remote_host, $remote_port);
 
#---  Remove remote hostname from URL
        $first =~ s/http:\/\/[^\/]+//;
    ($first, $method, $URL);
}
 
#-------------------------------------------------------------------------
#--     listen_to_port($SOCKET, $port)                                   --
#--                                                                     --
#--     Create a socket that listens to a specific port                 --
#-------------------------------------------------------------------------
sub listen_to_port {
    local ($port) = $_[1];
    local ($max_requests);
    local ($return_value);
 
    $max_requests = 30;          # Max number of outstanding requests
 
	#--- Get localhost name
	$local_hostname = `hostname`;
	chop($local_hostname);
 
    $_[0] = new IO::Socket::INET (
    						LocalHost =&gt; $local_hostname,
							LocalPort =&gt; $port,
							Proto =&gt; 'tcp',
							Listen =&gt; $max_request,
							ReuseAddr =&gt; 1
                                                       )  || die "socket: $!";
    local ($cur) = select($_[0]);
    $| = 1;                             # Disable buffering on socket.
    select($cur);
}
 
#-------------------------------------------------------------------------
#--     open_connection($SOCKET, $remote_hostname, $port)                --
#--                                                                     --
#--     Create a socket that connects to a certain host                 --
#-------------------------------------------------------------------------
sub open_connection {
    local ($remote_hostname, $port) = @_[1,2];
    local ($return_value);
 
#-- Get the port number by service name
    if ($port !~ /^\d+$/) {
        $port = (getservbyname($port, "tcp"))[2];
        $port = 80 unless ($port);				#If the service name is not valid, give it a default number(80)
    }
 
    my $remote_addr = (gethostbyname($remote_hostname))[4];
    if (!$remote_addr) {
        die "Unknown host: $remote_hostname";
    }
 
    #print "Connecting to $remote_hostname port $port.\n\n";
    $_[0] = new IO::Socket::INET (
    						PeerAddr =&gt; $remote_hostname,
							PeerPort =&gt; $port,
							Proto =&gt; 'tcp'
                            ) || die "socket: $!";   
 
	local ($remote_ip) = $_[0]-&gt;peerhost();
    print "Connecting to $remote_ip port $port.\n\n";								
 
    local ($cur) = select($_[0]);
    $| = 1;                             # Disable buffering on socket.
    select($cur);
}

Python版:

"""This is a simple HTTP proxy written in Python that imitate the Perl one:
 
http://liuj.fcu.edu.tw/net_pg/proxy-stu.pl.html
 
I wrote this in Python for classmates to compare to the one in Perl, because the
readability of Perl is awful. You can see the readaibility is much better than
the Perl one. You can also read this article:
 
http://www.garshol.priv.no/download/text/perl.html
 
to see what's wrong with Perl, and why Python.
 
To read this program, you might need the documents of python:
http://docs.python.org/index.html
 
You can also find older version here:
http://www.python.org/doc/versions/
 
To learn Python, you can browse my tutorials:
http://ez2learn.com/
 
If you got any problem about this program, feel free to ask.
 
Author: Victor Lin (bornstub@gmail.com)
        http://blog.ez2learn.com
 
2009/4/27 - Create first version
2009/4/30 - Fix the problem of keep-alive (fore the connection closed)
 
"""
 
import sys
import socket
import urlparse
import signal
import datetime
 
def listenToPort(port):
    """Listen to specific port and return socket
 
    @param port: port to listen
    @return: socket
    """
    # get name of host
    host = socket.gethostbyname(socket.gethostname())
    # create socket and bind, listen
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind((host, port))
    s.listen(5)
    return s
 
def openConnection(host, port):
    """Open a connection to remote host
 
    @param host: host to connect
    @param port: port to connect
    @return: connection socket
    """
    # create socket for connecting remote host
    remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ip = socket.gethostbyname(host)
    remote.connect((ip, port))
    return remote
 
def analyzeRequest(client):
    """Parse http request
 
    @param child: file of client socket to read request and parse
    @return: (first line of request for http server, http method, socket to url)
    """
    first = client.readline().strip()
 
    # parse first line
    method, url, version = first.split()
 
    # parse url
    result = urlparse.urlparse(url)
    host = result[1]
    if result[1].find(':') != -1:
        host, port = host.split(':')
        port = int(port)
    else:
        port = 80
    url = urlparse.urlunparse(('', '') + result[2:])
 
    # check is there a loop
    if host == socket.gethostname() or host == '127.0.0.1' or \
        host.lower() == 'localhost':
        print " --- Connection to proxy server ignored."
        print &gt;&gt; file, "It's not nice to make me loop on myself!."
        client.close()
        sys.exit()
 
    remote = openConnection(host, port)
    # the first line for remote socket
    first = "%s %s %s" % (method, url, version)
    return first, method, remote
 
def main():
    if len(sys.argv) &gt; 1:
        proxyPort = int(sys.argv[1])
    else:
        proxyPort = 5364
 
    ##--- Step 1 : Create Socket ---##
    s = listenToPort(proxyPort)
    ip, port = s.getsockname()
    print " --- Proxy server running on %s ( %s ) port: %d" % (
        socket.gethostname(), ip, port)
 
    ##--- Step 2 : Wait for request ---##
    while 1:
        print " --- Waiting to be of service ..."
        client, address = s.accept()
        clientFile = client.makefile()
        ip, port = address
        print "Connection from %s port: %d" % (ip, port)
        ##---  Step 3 : Read first line of request and analyze it. ---##
        first, method, remote = analyzeRequest(clientFile)
        ##---  Step 4 : Send request to remote host ---##
        remoteFile = remote.makefile(bufsize=0)
        # write first line to remote server
        print &gt;&gt; remoteFile, first
        print first
        # force the remote connection closed as finish this request
        print &gt;&gt; remoteFile, 'Connection: close'
        # read headers from client
        contentLength = 0
        for line in clientFile:
            line = line.strip()
            print line
            # filter headers that we don't want they passed to server
            match = False
            for header in ['connection:', 'proxy-connection:', 'keep-alive:']:
                if line.lower().startswith(header):
                    match = True
            if match:
                continue
            # get content length
            if line.lower().startswith('content-length:'):
                _, length = line.split(':')
                contentLength = int(length)
            print &gt;&gt; remoteFile, line
            if not line:
                break
 
        if method == "POST":
            if contentLength != 0:
                data = clientFile.read(contentLength)
            else:
                data = clientFile.readline()
 
            print "POST body:", data
            print &gt;&gt; remoteFile, data
 
        ##---  Step 5 : Wait for response and transfer it to requestor. ---##
        print " --- Done sending. Response: "
        print
 
        contentLength = None
        for line in remoteFile:
            line = line.strip()
            print &gt;&gt; clientFile, line
            if not line:
                break
            print line
            if line.lower().startswith('content-length:'):
                _, length = line.split(':')
                contentLength = int(length)
 
        if contentLength is not None:
            body = remoteFile.read(contentLength)
        else:
            body = remoteFile.read()
 
        clientFile.write(body)
 
        remoteFile.close()
        clientFile.close()
 
if __name__ == '__main__':
    main()

可讀性很明顯地Python優於Perl,我在這裡說明為什麼,原因其實很簡單,因為Perl加太多語法和用太多特殊符號在他的語言中,Perl對於正規表示法有語法,對於讀檔有特別的語法,對於向系統下指令有特別的語法,Perl對於這些瑣碎的功能加了太多的語法,使得用Perl寫出來的程式難以被簡單的理解,舉個最顯著的例子,就是他的特別變數,Perl有一系列$開頭的特別變數,在那程式裡就出現了幾次

$$
$|
$_

請問這三個特別變數在Perl中各代表什麼意思? 除非你真的對Perl很熟,否則答案肯定是我不知道,我得翻手冊,這就是為什麼Perl可讀性較差的原因之一,Perl的可讀性一直是很多人所詬病的,接著我們在裡面看到有一行很有趣的是有註解的,他這樣寫

$| = 1;    #Disable buffering

如果他不寫註解,我敢打賭沒幾個人能光看那一行知道這是在幹什麼的,因為我不太清楚它要disable buffering的原因,而disable buffering又是指哪個buffer? 網路? STDIO? 而且不那樣做也不會怎樣,所以我在Python版裡沒有照著寫這行,但是我們假設我們也需要在Python裡寫這行,那可能會是怎樣寫,我們假設他是要disable socket的buffering,而且我們假設Python有個函數叫disableBuffering(),所以我們相對應的Python版本就這樣寫

disableBuffering()

看出差別了嗎? 是的,Perl將這些符號做為特殊用途,加了很多特殊的語法,雖然讓程式較短較好寫,但是很明顯地讓可讀性大大地降低,在沒有查手冊之前根本沒辦法得知這一行到底是在做什麼用的,然而,因為用語法來實現太多功能,某種程度上算是不良的設計,因為他們都能夠用函式或函式庫來取代,而函式庫的取代雖然要寫多一點字,但是可讀性大大提升,光是看到disableBuffering()這樣的函數名稱就能很清楚地明白這一行是在做什麼用的,可讀性的差別就是這些語言的天性,也是判斷程式語言優劣的關鍵之一

可寫性

可寫性是指一款程式語言是否很容易撰寫

通常當一款程式語言很囉唆或不直覺時,他的可寫性會比較差,但相對的可讀性可能會比較好,反之亦然,以Perl為例子,如果你對那些符號很熟,你可以打很少的字,就寫出你要的功能,但是那樣犧牲了可讀性,但是其實某些情況下可讀性和可寫性都可以達到一個很不錯的平衡,我認為Python就是這樣的例子,Python因為語法限制性高,加上符號的定義也很簡短,所以寫Python的速度很快

可靠性

可靠性是指不易讓使用者犯錯,而犯錯也可以很容易的找出來

這比起可讀性和可寫性較難以解釋,但是我想這樣說,如果你受傷不會覺得痛,會發生什麼事? 答案很簡單,很快的你就會上西天,人類的痛覺是一種可以自我警告的系統,而程式的可靠性,所指的犯錯容易找出來也就是這個意思,試想一下,當你不小心打錯了程式,但是因為程式語言語法設計上很寬鬆,編譯一樣可以通過,他沒有警告你錯誤,程式可以執行,但是卻不是正確地執行,很明顯的後果就是開發者沒辦法輕易的得知這個問題,更糟的是因為沒有辦法偵測出錯誤,錯誤一樣還在那裡,如果你開發的是飛機的飛航系統,很快的你的程式在開發期間沒有讓你知道有問題,當真正運行時問題才暴發開來,這時麻煩就大了,這就是為什麼可靠性很重要的原因,舉個C語言的實例

int array[10];
int i = 13;
array[i] = 5; // oops

請問上面這段程式碼會有什麼樣的結果呢? 首先,這樣寫語法正確,所以可以執行,而C語言為了效能考量並不會對陣列索引的邊界做檢查,所以當你的程式跑到最後一行,對一個超出你陣列範圍的整數做指派會發生什麼事? 答案是不知道,很簡單的原因是,這array是放在stack裡的,你今天對他做存取,它會從它在stack中分配到的空間位置往下加元素個數乘上索引的大小,因為他不做檢查,所以只是忠實地去做,所以要看在stack中那個位置到底是什麼,那個倒霉鬼可能是呼叫函數的位址,可能是某人分配的區域變數,端看當時的環境是怎樣,被蓋掉的那塊記憶體的用途不同,發生的事情也會不一樣,這個叫做行為未定義,你永遠沒辦法確切的說出會發生什麼事,就像你拿一塊石頭從101往下丟一樣,會不會砸到人、砸到誰根本都是無法預知的事情,這整個程式碼所要呈現的重點就在於,因為為了效率考量,不做檢查,所以出錯也沒辦法知道,C語言就是這樣自由的語言,因為這樣的特性使它在低階的應用上受到歡迎,但是也因為這樣,很多新手程式設計師難以駕禦C/C++就是這個原因,因為C/C++太自由了,他們常常會怎麼死的都不知道

因此,做為一款嚴僅的程式語言,最好要能盡早在程式的編譯時期,就讓使用者知道出錯了,語法錯誤的檢查某些程度上來說是這樣的用意,C++ meta-programming的觀念裡,也有一條讓錯誤盡早在編譯時期被發現,與其等程式執行了才出錯,在編譯時期就發現才是比較好的,因為程式執行的不確定性比較多

可攜性

可攜性是指一個程式語言對於環境的依賴性有多低

我們都習慣使用Windows作業系統,但是事實上世界上還有很多種不同的電腦作業的環境,程式是要在這些環境中執行的,一個可攜性好的程式語言應該要和平台沒有關聯,或是做適當的介面來降低藕合度,舉個例子,你如果用VB6.0寫一個視窗程式,那麼他可以在Linux下面跑嗎? 就我所知的答案應該是不行,而相對於Python,Python的語言基本上是和平台無關的,如果搭配一個一樣可攜性很好的視窗函式庫就能寫出跨平台的視窗程式,能在Windows下跑,也能在Linux跑,所以相較之下Python的可攜性就比VB6.0來得好很多,大多數常見的程式語言可攜性其實都很不錯,從C/C++、Java、Perl、Python等都能在各大平台看到這些語言的身影,但是也是有某種程度上的差別,有些語言雖然有可攜性,但並不是完全地可攜,C/C++就是很好的例子,同樣一隻程式,在不同的環境下,很多時候你可能得視你編譯的環境、平台來對程式做某種程度上的修改,才能在其它平台執行,因為他是屬於靜態的語言,所以沒有辦法,靜態語言的可攜性基本上會比動態語言差

主觀的評估條件

上面提到的那些是客觀的評估條件,照程式語言裡的話,其實還少了成本和一些瑣碎的部份,但因為太囉唆就不在這裡多提,接著要介紹的就是主觀的程式語言好壞的評估條件,基本上我所學的程式語言課程沒有提到這部份,是我自己加上來的,因為我覺得這是很重要的一個部份,在評估完上面這些項目後,要來看的就是主觀的評估項目

語言哲學

我所能想到最主要也最重要的主觀評估條件就是語言背後的哲學,很多語言設計都有思想的,不是高興隨便亂設計的,他們很多都是從這些中心思想出發來進行設計,當遇到設計上的兩難問題時,他們會以這些中心思想來做為發想,並不是每款程式語言都有顯著的哲學口號,或許是我不知道,我在想brainfuck的哲學思想是』Fuck your brain』嗎?

但是有一些語言的中心思想很明確,而且他們也都照著這個中心思想在發展,有趣的是有兩個完全相反的中心思想,剛好是做為語言設計的哲學例子,那就是Perl和Python,首先來看Perl的哲學:

There’s more than one way to do it

他的想法就是,可以有很多種方法可以辦到同樣的事情,而Zen of Python,也就是Python的禪道剛好相反

There should be one– and preferably only one –obvious way to do it.

我對於Perl的思想不瞭解,我不太明白他們那樣想的理由是什麼,但是我很清楚Python他所想表達的,相較於Perl,因為同一件事情可以有很多方法達成的話,那麼通常依照人的性格,一百個人可能就會寫出一百種不同的程式,而且在這些程式中有大部份都是濫用的程式,程式碼也因為可能性太多,會難以理解,但是以相反的哲學來設計的話,只有一個明確的方法,那麼就直接使用吧,一來不需要花額外的時間浪費在思考非解決問題的關鍵上,接著也因為只有一個明顯的方法來達成,可讀性自然也較佳,因為顯而易見,這是我對Python禪到的見解,但是佛曰: 不可說,即然他名為禪我想意思就是要你自己悟,這就是一個程式語言背後哲學有趣的地方

然而,上面Python提到的這句話,並不是Python禪道的全部,而是做為中心思想很重要的一句,事實上Python的禪是由好幾句組成的:

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren’t special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one– and preferably only one –obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea — let’s do more of those!

這麼多的句子,以我快十年寫程式的經驗,我都能很認同這些話所說的,不過有些我還是不太明白,像是Simple is better than complex.就顯而易見的是KISS原則,而後面的Complex is better than complicated.就不太瞭解他所表答的意思

做為一款優秀的語言,Python有他精深的哲學做為發展的中心思想,而這思想也深得我心,所以我喜歡Python,Python的所做所為都能體現出他寫的這些,每個人認同的哲學可能會不太一樣,而做為認同的這部份,就是較為主觀的評估條件,做為程式語言的哲學,上面那些客觀層面討論完之後,剩下的就是這些思想,而思想就很難分對錯,這也是為什麼程式語言討論好壞最後都變成無止盡的爭論,因為他們的中心思想不一樣

結論

寫了這麼多,就是希望大家能夠瞭解程式語言要怎樣比較,不是說程式語言沒有好壞之分來當做不知道怎麼評估的藉口,又或著只會單靠喜好來決定程式語言

書籤:
  • Hemidemi
  • funp
  • MyShare
  • udn
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • YahooKimo
  • Haohao
  • TwitThis
Advertisement

13 comments

  1. EzGamer 說道:

    個人理解是
    Complex is better than complicated.
    這句的精神你已經了解並且在文章中說明了

    這句應該是指,有時候程式可以寫的很簡潔卻難以理解。這時反而用容易理解卻繁複的寫法比較好。

  2. francis 說道:

    愚见:
    每个语言都有自己的美丽之处,或许缺点正是有点呢?
    毕竟开发环境,应用环境都有可能是大相径庭的。

  3. Brian Hsu 說道:

    抱歉,Python/Perl 我都不是很了解,但覺得這個例子很有趣,想試著用其他語言寫一下。

    我想問一下 Python 裡 analyzeRequest 的這行:

    method, url, version = first.split()

    裡面的 version 是什麼?我大致上可以猜出 method 是 HTTP 的 GET/POST,url 是網址,可是 version 會是什麼?

    在 Perl 版本裡這一段的行為好像又不一樣,是切成 method / url / port 的樣子,然後又正規表示檢查是不是合法的 REQUEST?Python 版的好像沒有做這段檢查?

    my ($method, $remote_host, $remote_port) = ($first =~ m!(GET|POST|HEAD) http://(^/:]+):?(\d*)! );

  4. victor 說道:

    @francis:

    每個語言都有他優缺點,但是很多時候是在同一個環境、同一個目的、同一個作業系統…. 等等,在這樣的情況下,程式語言的好壞還是得比較不是嗎? 這些其實都沒有所謂的最好或最壞,但是當情境確立之後,程式語言的好壞通常很容易分得出來

    很多人說一切事在人為,又說工具只有適合和不適合,這是我矛盾,既然一切事在人為,那又為何需要工具呢?

  5. victor 說道:

    @Brian Hsu:

    Python也有正規表示法,我選擇不用,一來是因為沒必要,有更好的方法寫,二來是那樣比較難讀,正規表示法的可讀性很差

    split()是把一個字串用某些字分開,預設應該是空白之類的

    所以HTTP的Header是
    GET / HTTP1.1
    像這樣,經過拆開後,分別變為
    GET
    /
    HTTP1.1
    再分別指派給
    method
    url
    version
    所以你看到第三個就是HTTP1.1,也就是HTTP協定的版本那串字

    下面有用到urlparse來解析網址,而如果某種程度的不合法header出現的話,通常在處理過程中,都會丟出例外,然後程式中止,像是網址不合法,或是split切出4個或2個結果

    method, url, version = (1, 2, 3, 4)
    method, url, version = (1, 2)

    因為Python嚴僅,所以他不允許拆開個數不合的串列,透過這些一樣可以檢查合法性,所以我沒有特地寫出來

  6. Ricky 說道:

    舉個例子
    很多人會覺得goto是個萬惡不赦的東西
    為何php5.3又要把goto加進來??
    Bad:
    if((!Validate(‘Account’))||(!Validate(‘Email’))||(!Validate(‘Password’)))
    //Oh It’s a bad member…
    // do something…
    else
    //OK…It’s a good member…
    //do domething

    Good:
    if(!Validate(‘Account’)) goto err;
    if(!Validate(‘Email’)) goto err;
    if(!Validate(‘Password’)) goto err;
    //OK…It’s a good member…
    //do domething
    err:
    //Oh It’s a bad member…
    // do something…

    上面的Bad例子,光是()可能就讓人頭昏腦脹
    誰說goto很糟糕??
    用的好,程式結構簡單明瞭。
    即使是結構嚴謹,用的不好,還是讓人看得一頭霧水。

  7. victor 說道:

    @Ricky:

    我會這樣認為,做為低門檻的程式語言,理應當盡力避免容易爁用的特性

    goto的問題是源自於以前的程式語言,很多都是用goto在寫的,寫出所謂義大利麵程式,一個程式任何地方都可能是進入點,任何地方都可能是離開點,那樣會使得程式難以除錯,和維護,所以後來有人證明了,單一入口和單一出口的結構化程式設計可以完全取代goto

    基本上你這個例子完全沒有使用goto的必要,可讀性和可寫性並沒有因此而改善多少,你需要的是好的風格排版,讓那個if看起來更清楚

    真正會需比較需要goto的情況是多層迴圈,要從裡面跳出來如果不用goto就需要較多變數和判斷,但是有人這麼認為,當你的迴圈層數多到一定程度時就表示你該重構了,甚至有人認為迴圈最多不應該超過兩層,我個人認為到了三層程式就開是難以理解,四層就太超過了

    即然我們不需要這麼多層的迴圈,那麼為什麼我們需要goto? 增加語法是好是壞,如果PHP只是為了討好其它語言的使用者,像是C/C++,而認為該加goto,那麼我會認為這實在很糟糕,PHP很多決定都只是為了討好不同族群的使用者,把他們拉進來後,然後呢? 我想PHP少了一個設計的哲學思想,或許是我不知道,有沒有人可以告訴我PHP背後的哲學和思想是什麼?

  8. Brian Hsu 說道:

    我自己在寫 PHP 的時候,感覺他給我的哲學就是『程式會動,網頁出得來就好』。XD

  9. francis 說道:

    @Victor
    个人体会到的PHP思想就是自由随意开放。
    自己写的时候不用担心是否符合PHP理念,因为任何一种写法都会被有效地执行,只要自己喜欢就好。

  10. citypig 說道:

    大部份的文章我都可以認同,但拿 Perl 與 Python 這兩個有點極端的語言來說明「如何評估比較程式語言」我覺得不太妥當:

    1. 「程式被讀比被寫還多次」?!
    程式只有系統在執行時會去讀,寫得人繼續寫他的程式。那讀比寫還多的原因是什麼? 維護又是要維護什麼?

    2. 「可讀性很明顯地Python優於Perl」該由誰來評斷? 寫 Perl 的人? 寫 Python 的人? 讀中文系的人? 讀外文系的人? 其它人?
    碰巧我就是用 Perl 在寫程式的,Perl 的程式中運用了很多符號,這是 Perl 的特點,可以加快寫程式的速度,喜歡的人就會喜歡,不喜歡的人就不喜歡,要讀得懂當然要先熟悉 Perl,我想任何語言皆同。
    你舉的例子我反而覺得 Perl 寫得很好,重點是「註解」寫得很清楚,這才是最重要的。你不也是看著他的註解才寫出程式的?!

    3. 拿 Perl 的 『$| = 1;』 跟 Python 的 『disableBuffering()』 來比較也怪怪的!
    你既不懂 『$| = 1;』 的原意是什麼,而 Python 也沒有 『disableBuffering()』,那你到底在比較什麼?
    原 Perl 的作者如果願意,也可以定義一個函式叫 disableBuffering() 內容就只有一行 『$| = 1;』 不是嗎?

    4. 批評 Perl 的人常說「一百個人可能就會寫出一百種不同的程式」?!事實並不會這樣,Perl 只是某些語法可以簡化、倒裝句…允許有不同的寫法而已,懂 Perl 的人還是看得懂。
    而 Perl 這語言本身沒有什麼太大的缺陷,純粹是你不喜歡而已,Python 的「嚴謹度」或 Perl 的「自由度」,我覺得是沒什麼好比的。而「There’s more than one way to do it」,真實的世界就是這樣。

  11. victor 說道:

    @citypig:

    1.
    我不知道你是不是有誤會,又或著我誤會你想說的,那是指人去讀,不是系統去讀

    首先,在寫程式的過程中,其實我們就一直在重覆的讀程式,在debug的過程也是在讀程式,請仔細回想寫程式的過程,真正花在』寫』的時間有多少? 次數又有多少? 相較於讀,讀是不段地發生,你在改bug時眼睛是一直盯著程式碼看,要在裡面找出問題,trace時也是,而寫只有把你想法轉成程式碼打出來的那時才發生,所以程式碼被讀比被寫多,我想答案應該很清楚

    這還只是單人寫程式,如果是多人呢? 像是雙人合作的模式,又或著團隊的開發,程式碼不是寫完就沒事了,嚴僅的做法應該是還得被review,程式碼沒review,你怎麼知道團隊中的某個人是否在裡面加了某些料? 又或著他寫的程式真的有符合標準嗎? 在這些過程中都是大量的閱讀

    接著,就是維護,當你要改一個人的程式碼時,你不懂他的程式碼是無法修改的,要不看一個人的程式能進行修改,我想情況大概只有那個人的程式都有經過良好的封裝、設計、寫文件等等,以oo的原則來說,open-closed原則來看,真的能做到的話,只要看他對外的介面文件之類的不用動到原本的部份就能修改,但是那樣完美的程式碼不存在,因此多少會有需要修改到原本程式碼的部份,這時如果你讀不懂他的程式,可能只有一個選擇就是重寫

    所以,以上這些就是程式碼被讀比被寫來得多的道理

    2.
    是的,那個程式寫得很不錯,有不少註解,但是也有不少沒有註解的地方,我有說過即使那些是相對客觀的條件,但是也是有主觀的成份,而且其實那樣是可以統計的

    如你所說的,我們可以找100個讀外文系的人,100個寫C/C++的人,100個寫Python的人,100寫Perl的人,分別讀兩份perl和python寫的程式然後做選擇題回答這程式的相關問題,例如某段程式是在做什麼的,在這樣的情況下有數據我們就可以更客觀的知道不同人對不同語言可讀性的好壞感覺

    我覺得這倒不是喜歡和不喜歡的問題,可讀性好,不管你喜不喜歡都容易讀懂,就像假設一個人討厭國文課他就讀不懂國文嗎? 不見得吧?

    好的可讀性應該是要顯而易見,perl對我而言最大的問題不在於它的語法,而是記憶的問題,請問$$ $% $^ $&那麼多種特別的符號你能記下多少種? 因為他和我們用的語言一點關聯、甚至是暗示都沒有,連猜的機會都沒有,需要靠純脆的記憶或是查資料來得知,當你要查的資料越多,這不是表示可讀性比較差嗎? 你讀的過程中一直需要停下來查語法

    還有語法太多也是個問題,像<>讀檔、`ls`是執行system的指令,~= /xxx/ 是regular expression的比對,還有很多鎖碎的語法,對我來說,每種功能都是語法,第一次看見這些語法,甚至看了好幾次,我還是得查手冊才知道這是在做什麼的,因為難以記憶,而且語法的手冊特別難查,不像是函式庫是英文字詞,你搜尋也難以搜尋,所以就算同樣是查資料,perl也會比較吃力,因為像你在google搜尋 perl <> 找不到相關的資料….
    但我搜尋 perl readline 可以找到

    總合以上種種原因我做出perl可讀性較差的判斷,你可能不認同我所說的,那我再舉一個例子,就是我們熟知的ip和domain name

    111.168.1.34這樣的數字,對人類一點意義都沒有,你讀了也沒辦法猜出這個網站是在幹麻,也難以記憶,但是domain name就不一樣了,他是有意義的東西組成,除了好記也能猜,像是www.sex.com,這樣再明白也不過了

    Perl的情況就像是一堆功能全是ip數字,難以記憶和猜想、理解,它把太多功能做成語法了,我認為那些本來都應該做成函式庫的,所以造成難以閱讀

    3.
    我想表達的是,Perl的寫法那樣並不能從寫的東西看出任何暗示,因為他放太多功能到語法上面了,$|是什麼意思在查手冊前不知道,但是如果是函數名稱就很容易猜,如果他有良好命名的話,當然,你會說,perl也能定義那樣的函數,問題是perl沒有,而大部份的perl程式都大量依賴$| $% $^那類特別變數的操作來完成某些目的,所以如果perl和python同時不寫註解,絕對會是perl比較難以理解,因為python沒像perl那樣多的語法,它只有完備的核心語法的功能,該是library的東西就是library,而perl對我來說,我覺得他太貪心了,把一堆東西都做成語法,暗示、字詞的意義全被拿掉了,只剩符號,無從猜起和難以記憶,那是我那段想表達的

    4.
    關於這點,我把哲學放在主觀的分類裡,就是這個原因,後面雖然我說我比較喜歡Python的哲學,但是不代表Python的哲學就比較好,因為即然是哲學和思想,本來就難以有對錯好壞之分,要看每個人的想法是否和那個一至

    我也同意你說的Perl語言本身沒有太大缺陷,我上一篇文章裡面就有人有提到,eq和==是分別的判斷運算子,因此可以分清楚程式設計師的用意,perl以語言的設計層面很完善,只是和Python的哲學不同,當這部份來到哲學層面的話,除非能證明對方的哲學是錯的或是有問題的,否則也只能陷於無限的爭論

    所以對於Perl的哲學雖然我不喜歡,但是我沒說他錯,那是不同的想法

::...
免责声明:
当前网页内容, 由 大妈 ZoomQuiet 使用工具: ScrapBook :: Firefox Extension 人工从互联网中收集并分享;
内容版权归原作者所有;
本人对内容的有效性/合法性不承担任何强制性责任.
若有不妥, 欢迎评注提醒:

或是邮件反馈可也:
askdama[AT]googlegroups.com


点击注册~> 获得 100$ 体验券: DigitalOcean Referral Badge

订阅 substack 体验古早写作:


关注公众号, 持续获得相关各种嗯哼:
zoomquiet


自怼圈/年度番新

DU22.4
关于 ~ DebugUself with DAMA ;-)
粤ICP备18025058号-1
公安备案号: 44049002000656 ...::