1.5 要制作怎樣的語言
1.5.1 要設計怎樣的語法
編程語言有很多種,C、C++、Java、C#等都是面向過程的編程語言(C++、Java、C#雖然也被稱為面向對象,但可以把面向對象看作是面向過程的一個派生)。目前看來,雖然面向過程的語言是主流,但還存在Haskell、ML這樣的函數式編程語言。函數式編程語言就是“變量值無法被更改”的一種語言。
對于已經習慣了面向過程語言的人來說,肯定會想“變量值無法更改還怎么寫程序呀”。其實這類語言已經編寫出了很多實用的程序。在函數式編程的基礎上發展出了如Prolog這樣的邏輯編程語言以及被稱為并行程序設計語言的Erlang。
不過目前被廣泛使用的仍然是面向過程的編程語言,本書中的代碼示例使用的也都是面向過程的語言風格,當然里面還會加入面向對象的一些功能實現。在本書中,除了會有C++、Java、C#這種基于類的面向對象之外,也會涵蓋類似JavaScript這種沒有類的面向對象。
語法層面上,會使用類似C語言的風格。crowbar的示例代碼如代碼清單1-1所示,Diksam的示例代碼如代碼清單1-2所示。
代碼清單1-1 crowbar版FizzBuzz
for(i = 1; i < = 100; i++){ if(i % 15 == 0){ print("FizzBuzz\n"); } elsif(i % 3 == 0){ print("Fizz\n"); } elsif(i % 5 == 0){ print("Buzz\n"); } else { print("" + i + "\n"); } }
代碼清單1-2 Diksam版FizzBuzz
int i; for(i = 1; i < = 100; i++){ if(i % 15 == 0){ println("FizzBuzz"); } elsif(i % 3 == 0){ println("Fizz"); } elsif(i % 5 == 0){ println("Buzz"); } else { println("" + i); } }
順便說一下這個名為FizzBuzz的小程序,其運行機制如下:
輸出從1到100的數字,如果為3的倍數時,則將數字替換為Fizz,5的倍數時則輸出Buzz,同時為3與5的倍數時輸出FizzBuzz。
這個小程序引自下面的文章。文章大意是建議企業在面試程序員時,至少應聘者能寫出這種程度的代碼再考慮錄用。
看了示例就能明白,無論crowbar還是Diksam,都是與C語言非常類似的語言。
如上所述,本書雖然會創造一門新語言但仍然會用到C語言,所以本書所面向的讀者應該是已經掌握了C語言的(還沒有掌握的人可以先去學習一下)。因此如果選擇C語言風格的語法,讀者應該會感到很親切,更重要的是筆者本人已經習慣了Java、C#這種以C語言為基礎的編程語言。
C語言是很老的語言了,這門語言不是在前期經過嚴謹的設計,而是在項目中一邊實踐一邊慢慢發展起來的,因此語法上難免有很多考慮不周的地方。比如在C語言中賦值使用 =,即數學中的等號。而C程序員在初學者階段編寫if語句時,肯定免不了會寫成這樣:

這樣慘痛的教訓至少也要經歷一次吧。賦值在Pascal等語言中,一般使用:=。如果讓一個沒有編程經驗的人來學習,Pascal這種語法應該更加友好一些。
不過我現在是要制作一門新的編程語言,而使用這門新語言的人應該都已經習慣了C語言的運算符,如果這里將賦值運算符定為:=的話反而會引起混亂,說不定我自己就先頭暈了。所以經驗之談是,語法上的些許優劣還是要給“習慣”讓步的。
——出于這種考慮,我最終決定制作一門與C語言類似的編程語言。
決定語法風格是編程語言創造者的特權。如果顧慮用戶習慣,可以參考并整合已有的編程語言。當然,也可以完全不考慮用戶的感受,去創造一門“理想的語言”。雖然我是以C語言的語法為基礎,但還是想到了以下幾點可以改進的地方。
1. if條件在C語言中,如果按條件執行的語句只有一句,則 {}可以省略。但是這經常會造成混亂,很多項目的編碼規范中都會規定必須包含 {}。因此最好在語法層面直接將 {}設置為不可省略(crowbar、Diksam均如此)。
2.既然已經將if條件中的 {}設置為不可省略,那么if后面的()要怎么辦呢?(關于這一點,我起初在crowbar中嘗試了一下省略if的括號,結果發現在crowbar中()是不可省略的。)
3.伴隨著語言的逐步完善,考慮到要增加一些關鍵字(參考2.3.1節的補充知識),此時再處理與已存在程序的變量名相沖突的問題就比較麻煩,所以考慮在所有的變量前加上 $(Perl或PHP等的解決方式),或者將關鍵字全部以大寫字母開頭(Modula-2等的解決方式)。
4. switch case語句中,最好能去掉忘了寫break就會進入下一個case這種容易產生問題的設計(Java沒有改進這一點,C#則做了一些半吊子的改進)。
5. switch case語句中,如果沒有進入任何一個case條件分支,也沒有寫default分支,那么在運行時直接報錯會不會更好一些(Pascal就是這樣處理的)?
6.編碼規范通過縮進來約束怎么樣?比如像Python那樣通過縮進來表明邏輯結構。
7.對于我來說,閱讀Python風格的代碼還有些吃力,因此是不是做成像C語言那樣用花括號包裹語法塊、把強制縮進的檢查交給編譯器去做比較好呢?
我希望讀者朋友們也能夠用好語言開發者的特權,不斷去追求“更加理想的語言”。呃,雖然我這樣講可能會被說成是站著說話不腰疼吧。
1.5.2 要設計怎樣的運行方式
程序員中應該無人不知,編程語言有編譯型語言和解釋型語言兩種。
編譯型語言中,C和C++比較有代表性。這類語言通常會將程序員編寫的程序源代碼,最終輸出為機器碼的可執行文件。
但是想要輸出機器碼的話,必須首先掌握機器碼才行。即便學習了機器碼并寫出了編譯器,該編譯器也無法輸出供其他型號CPU運行的文件。
這類生成機器碼的編程語言的優點是運行速度非常快,但是編譯器性能優化的相關技術,學習起來非常有難度。另外,在自制編程語言的理由中曾經列舉了“可以用編程語言擴展應用程序”這一點,而輸出機器碼的編譯器并不適合這個用途。因此本書中會選擇解釋型語言。
雖說“解釋型語言”只是一個詞,但是其實現方法又分很多種。
解釋型語言的“解釋”一詞源自英語的interpreter,是“能進行翻譯的物體”的意思。編譯器將源代碼翻譯為機器碼,之后CPU直接運行機器碼就可以了。與此相對的解釋型語言,則將程序員編寫的源代碼通過解釋器這一程序一邊解析一邊運行——這種公式化的定義看起來只有簡單的兩個步驟,但現實中幾乎不存在這么單純的解釋型語言(DOS的批處理腳本或UNIX的SHELL腳本是最接近解釋型語言的定義的)。雖說名為“解釋型語言”,但其中的大多數都會將源代碼臨時轉換為某種中間形態。
比如有代碼清單1-3這樣的代碼。
代碼清單1-3 簡單的if語句
if(a == 10){ printf("hoge\n"); } else { printf("piyo\n"); }
從機器的角度看,源代碼其實只是一些文字的排列組合而已,機器是無法直接運行的。現在大多數編程語言,都會將代碼轉換成一種叫分析樹(parse tree,也叫語法分析樹或語法樹)的東西。上面的代碼如果做成分析樹,則如圖1-1所示。

圖1-1 分析樹示例
Perl、Ruby等語言,一旦將代碼轉換為分析樹后,分析樹將無法再還原回源代碼。
本書第2章以后所用到的語言crowbar就是采用這種運行方式的語言。
對于這類語言來說,從源代碼到分析樹的構建過程還是得稱為“編譯”。但是這里的編譯器是在程序啟動時自動執行的。由于分析樹會生成在內存里,因此不會生成目標代碼或目標文件,所以程序員(用戶)一般意識不到有編譯器在執行。這類語言如果存在語法錯誤,會在剛開始運行時就被報出來,這正是源代碼被一次性全部讀入并構建分析樹的證明。如果是純粹的解釋型語言,如批處理腳本或SHELL腳本,則會運行到有語法錯誤的地方才會報錯。
那么,相對于Perl、Ruby這樣的運行分析樹型語言,在Java等語言中,取代分析樹的則是更底層的字節碼,然后通過解釋器運行字節碼。字節碼只是一些簡單的數字排列,為了盡可能地讓人讀懂字節碼,字節碼中的所有指令都被加上了一些名為助記符(mnemonic)的字符,代碼清單1-3的源代碼經過這樣一番處理之后最終會變成代碼清單1-4的樣子(源代碼中的printf改為System. out.println,并使用javap輸出)。
代碼清單1-4 Java的字節碼
0: bipush 10 2: istore_1 3: iload_1 4: bipush 10 6: if_icmpne 20 9: getstatic 12: ldc 14: invokevirtual 17: goto 28 20: getstatic 23: ldc 25: invokevirtual
本書第5章以后所用到的語言Diksam,就是采用這種運行方式的語言。
在Java中,編譯器生成的字節碼會被保存在class文件中。但是在Diksam中,編譯器會在程序啟動時執行,因此字節碼保存于內存中,不會生成類似class文件的東西。由此可以看出,從用戶的角度出發,不需要意識到Diksam內部其實有字節碼在執行。Python也是使用了類似的處理機制。
補充知識 “用戶”指的是誰?
前文曾寫道“因此程序員(用戶)一般意識不到有編譯器在執行。”
通常來說,用戶是指使用程序員編寫的程序的人,但是在這里,因為我們是要制作一門編程語言,所以本書中的用戶應該是指使用我們制作的編程語言的人,即程序員。
這種指代在操作系統、類庫、編程語言等面向程序員的文檔中經常出現,不過可能有讀者會有誤解,在此特別補充說明一下。
補充知識 解釋器并不會進行翻譯
在很多入門書中,提到編譯器與解釋器時,一般會采用以下說明:
編譯器會將源代碼一次性全部翻譯為機器碼。
與此相對的解釋器,不會事先做一次性翻譯,而是在運行的同時,逐行分塊地將源代碼翻譯為機器碼。
請允許我說句老實話,這樣的說明是完全錯誤的。
解釋器會將源碼或分析樹解析為字節碼這種中間形態,并且一邊解析一邊運行,但是解釋器并不會將源碼翻譯為機器碼。
Java或.NET Framework都具備在運行的同時將字節碼轉換為機器碼的功能,這叫作“JIT(Just-In-Time)編譯”技術,而這部分技術并不屬于解釋器。
那么解釋器具體是如何運行程序的呢?讀到后面你就會明白了。
- 從零構建知識圖譜:技術、方法與案例
- Python科學計算(第2版)
- 程序員面試算法寶典
- 青少年美育趣味課堂:XMind思維導圖制作
- 小程序開發原理與實戰
- 深度學習:Java語言實現
- 智能搜索和推薦系統:原理、算法與應用
- Practical Game Design with Unity and Playmaker
- SQL Server 2008 R2數據庫技術及應用(第3版)
- 大數據時代的企業升級之道(全3冊)
- Continuous Delivery and DevOps:A Quickstart Guide Second Edition
- 寫給青少年的人工智能(Python版·微課視頻版)
- R語言實戰(第2版)
- Python自動化運維:技術與最佳實踐
- C++ Windows Programming