在參與這個討論 http://www.iteye.com/topic/33971 后,這段時間對這個話題有了一些新的思考,寫下來和大家分享分享。
重點探討一下動靜態編程語言的語義,兼帶些DSL及通用語言,以及新手上手難易問題。
編程語言的語義,在論壇里討論不多。在這里先分析一下幾門主流靜態語言,C,C++,Java,C#的語義。這些語言從編程風格角度講,都稱之為”imperative programming language”,(命令式的編程語言)。究其原因,這些語言都是對計算機的核心部件,CPU及內存,施發號令的。
1. int a = 4;
2. int b = 4 + a;
3. int c = 5.2345;
第一句,具體語義是,在內存里分配一塊內存,大小為4 bytes,在這塊內存里,寫入4。第二句,具體語義是,在內存里分配一塊內存,大小為4 bytes,從a中取值,和4進行加法運算,結果寫入b指向的4 bytes內存。第三句就是個潛在的錯誤,等號右邊是個8 bytes的double,把8 bytes的數據寫到4 bytes的內存塊里去,數據會損失的。
要把這些靜態語言內存分配的經驗照搬來理解動態語言,完全是搞錯了方向??纯聪旅嬉欢蝚avascript代碼:
1. var a = 5;
2. alert(a);
3. var a = "foobar";
4. alert(a);
這是一段完全合法可以正確運行的javascript程序,然而對于只編過靜態語言而且對靜態語言的語義很了解的人,卻很難理解。變量a,明顯不是指向根據類型分配出來一塊大小固定的內存塊。
如何理解這一段代碼的語義?
Revised Report on the Algorithmic Language Scheme 一文里有這么一段:
引用
Scheme has latent as opposed to manifest types. Types are associated with values also (also call objects) rather than with variables. (Some authors refer to languages with latent types as weakly typed or dynamically typed languages. Other languages with latent types are APL, Snobol, and other dialects of Lisp. Languages with manifest types (sometimes referred to as strongly typed or statically typed language) include Algol 60, Pascal, and C.
Paul Graham在其“What Made Lisp Different”一文中這么說:
引用
A new concept of variables. In Lisp, all variables are effectively pointers. Values are what have types, not variables, and assigning or binding variables means copying pointers, not what they point to.
這兩段合在一起,可以正確理解動態語言的語義。
靜態,變量實際是分配的內存塊,大小固定。

動態,變量實際是個指針,可指向內存任何一塊。
(當然是運行的不同時期指向不同的內存塊)
看看下面幾句:
1. JavaScript: var a = 5;
2. ML: val a = 5;
3. Scheme: (define a 5)
這些語句應該理解為, (等號右邊)表達式evaluate出來一個值,這個值綁定到變量a里面去。用來描述上述代碼語義的正確的詞是binding。
看看下面ML語言解釋器對ML代碼的解釋:
1. Moscow ML version 2.01 (January 2004)
2. Enter `quit();' to quit.
3. - a;
4. ! Toplevel input:
5. ! a;
6. ! ^
7. ! Unbound value identifier: a
8. - val a = 5;
9. > val a = 5 : int
10. - a;
11. > val it = 5 : int
12. - val a = "foobar";
13. > val a = "foobar" : string
14. - a;
15. > val it = "foobar" : string
注意第七行的提示。
第十行,第十四行光打入a,也是個表達式,evaluate出來的值,綁定給省缺變量it。
看看下面Scheme語言解釋器對Scheme代碼的解釋:
1. > a
2. ; Unbound variable: a
3.
4. > (define a 5)
5. ; Value: a
6.
7. > a
8. ; Value: 5
注意第二行的提示。
一定要分清動態語言的變量綁定和靜態語言的變量賦值的區別。變量是一個數學上的概念,在靜態語言中,叫變量其實不合適,還不如直接叫a memory box,更能清楚地說明其本質。
對于靜態語言,弱類型是致命傷,因為在聲明變量的時候,內存塊已經分配好了,往這個內存塊里寫一塊內存塊存儲不下的數據,帶來的傷害是致命的。對于動態語言,強弱類型未必重要。
在C/C++/Java/C#里面,內存是可以分配到Stack里面,也可以分配到Heap里面, 程序員一定要搞清楚區別, 像在C里:
1. int a = 5;
2. int b[] = { 1, 2, 3, 4}
3. int* ptr = (int*)malloc(10*sizeof(int));
a 和 b 所分配的內存都在stack里,c 指向heap里的一塊,退出前不把c 給free掉,就會遺漏內存。給function傳值的時候,更要小心,傳a是把5這個值給傳過去,傳b是傳b這個array第一個元素的地址。
到了C++,更加繁瑣,因為C++的 Object是可以分配在stack上的,隨便寫幾句代碼,都會用到assignment operator = , address-of operator &, copy constructor.
1. const ClassFoo e1; // default constructor, destructor later
2. ClassFoo e2(e1); // copy constructor
3. e2 = e1; // assignment operator
4. ClassFoo *pe2 = &e2; // address-of operator (non-const)
5. const ClassFoo *pe1 = &e1; // address-of operator (const)
C++編譯器自動生成這些函數,有時不符合需要就要自己手寫。
Java里面所有的object allocation, 都是分配在Heap里的,光這一點,就大大減輕了編程的繁瑣度。從Java轉向C++的朋友,一定要記住這一點。C++的 Object是可以分配在stack上的。
Java里面的primitive變量是分配在Stack上的,其實如果廢除這八個primitive types,全部用Object reference,動靜態語言的差別已經不那么大了。Type inference在C# 3里面,已經開始實現了:
1. var str = "Hello world!";
2. var num = 42;
3. var v = new TypeWithLongName<AndWithTypeParameter>();
歐美計算機專業的第一門語言,一般是ML或Scheme。這些語言,做到了程序員不用思考內存是分配在stack上還是heap上,內存回收由GC管,因而可以集中精力,學習算法,遞歸等等。
用編程來解決問題,需要三方面的技能:1. 對編程語言,語義及運行環境的掌握,2. 對解決問題的算法的掌握,3. 擁有寫出結構清晰,簡潔易懂的代碼的能力。
第一點和第二點經常交匯在一起,因為語言,經常是為了解決某個領域的問題而設計的,解決算法,遞歸之類的問題,用functional programming language,操作系統,應該用C,web領域之PHP,科學計算之Matlab,試驗儀器控制之labview,關系數據庫之SQL,莫不如此。
那么什么算是通用語言,什么算是DSL?通用不通用是相對的。C是一門通用語言,但也可以說是操作系統的DSL。從某種角度來說,能夠全面控制計算機的,才叫通用語言,那么只有匯編才符合這個條件,C和C++勉強算得上。
新手上路,該學什么?應該從某個領域學起,學習解決那個領域問題需要的方法,而且學習那個領域的DSL。這樣成效出的最快,而且不受干擾。
現在學校里教學靜態語言占主流,有歷史原因。以前計算機不夠快,用C編程是唯一的選擇?,F在對運行效率要求很高的領域,還得用C,C++。但是在很多領域,這已經不是個問題了。由于歷史的慣性,靜態語言還在繼續教。學校老師學新知識的動力,可不大。這些老師教出的學生,只會靜態語言,那么公司為了保證人手充足,也會傾向靜態語言。這種狀況,慢慢會打破。