當你在糾結學Python還是Java時,大二學生已經開始造編程語言了!

當你在糾結學Python還是Java時,大二學生已經開始造編程語言了!

導讀

我在之前的文章中說過多次,大學生在校期間應該去搞定那些基礎知識,因為大學生沒有工作的壓力,有大塊的時間,基礎知識相對枯燥,要想有成就感,可以做一些簡單的系統,例如一個簡單的編程語言,一個有基本功能的OS......

楊韜是我的知識星球“碼農翻身”的一個大學生,他在星球提到做了一個簡單的編程語言解釋器,我建議他把過程給寫出來, 就是這篇文章了。

下文的“我”就是楊韜。


為什麼要自己寫一個解釋器?

從大學開始學習編程, 現在已經快兩年了, 接觸了不少的編程語言。最開始入門學了C語言; 後來想寫安卓軟件, 學了Java; 接著接觸後臺開發,學了Python; 後面又陸續地接觸Go, Dart, C++。 仔細算算, 已經接觸過6門語言了!

但是仔細想想我似乎又什麼也沒有學到,過年回家的時候, 遇到一個對計算機很感興趣的四年級的小朋友(ps: 小朋友會寫一點點python, html, 現在的小孩也太強了), 問了我諸如計算機上面的程序是怎麼跑起來的, 代碼是怎麼運行這種看似很基礎的問題, 可悲的是我居然對這些問題似懂非懂, 不能給小朋友很清楚地解釋出來。

我都不好意思說自己是學計算機的了, 居然連這些基礎的問題都沒有搞清楚。 這是促使我去深入學習編譯原理, 計算機組成原理和操作系統這些基礎知識的重要原因。

學習編譯原理最簡單的方法(對我來說)大概是自己實現一門編程語言, 雖然費時費力, 不過能對整個過程有個清晰的瞭解。

另外一個重要原因是有一種想要自己寫一門語言的衝動。 尤其是在學了這麼多門語言之後就會萌生出這樣的念頭。不同的語言有不同的讓我喜歡的特性: Python有漂亮簡潔的語法, 靜態語言Go實現了像動態語言一樣的鴨子類型的接口, Dart有很多語法糖和方便的異步… (當然這些特性是仁者見仁, 智者見智的)。

但是又不能找到一門語言,具備所有自己喜歡的特性, 那就自己寫一個好了,可以把自己喜歡的特性都加上。


學習的過程

學校的編譯原理的課程安排在了大三, 我還沒有學過, 所以一切都是從0開始。 我先看了前橋和彌寫的《自制編程語言》一書, 這本書的實戰性很強, 沒有介紹太多的理論知識, 而是直接教你怎麼寫編程語言。

我從這本書中瞭解了寫一門編程語言的大致過程和大致的思路。 不過書中的很多解釋不夠充分, 對於完全沒有接觸過編譯技術的人來說還是有點費解(也可能是我自己理解能力不夠好)。

我也是在自己瞭解了大致思路後就開始自己嘗試寫, 然後再回過頭來看書, 看作者提供的源代碼, 才能比較好地瞭解作者是在幹嘛。

還有一些書中內容介紹的不夠充分, 比如yacc和lex的使用。 這種工具畢竟比較流行, 網上找找別人寫的博客多看看, 自己再多嘗試就能很好地掌握。

因為更喜歡在實戰中學習, 所以前期只是瞭解了大致思路沒有特別深入的學習理論知識, 就直接開始碼代碼了。 在具體實現的過程中遇到問題, 再去看書或是網上尋找答案。


設計和實現

我選擇寫一個動態語言的解釋器, 而不是靜態語言的編譯器。

之所以要寫解釋器, 不是因為我更偏好動態語言, 其實相比而言我更喜歡靜態語言。 真正的原因是, 我覺得這只是第一次嘗試, 很多東西都不會, 肯定會寫得很爛的,不如先就寫動態語言, 等真正學得比較好了, 再回過頭來寫一門自己喜歡的語言。

正式開始寫代碼前, 我還要給這門語言取個名字,雖然只是個練手的項目, 不過還是得有個名字吧。 取名字還正不是一件容易得事, 就像給函數或者類取一個恰如其分的名字一樣。

聽說恰當的函數名或類名還能反應整個項目的設計是否合理, 邏輯清晰, 語言的名字似乎並沒有這樣的意義。

我腦袋裡閃過的第一個名字是Cactus(仙人球, 仙人掌)。 我覺得很喜歡這個名字, 就把Cactus暫時留個我要寫的靜態語言了(希望我真的會寫, 沒有白留)。 仙人球是植物(正好是靜態的), 同樣帶刺的動物是刺蝟(Hedgehog)。 動物是動態的, 正好符合我要寫的動態語言, 於是就叫Hedgehog了。

前面提到了lex和yacc, 我在自己寫的編程語言裡面也使用了這兩個工具做詞法分析和語法分析。 既然是自己要寫一門語言為什麼還要用別的工具呢? 當然不能以”不重複造輪子”作為藉口, 我就是為了造輪子才想要自己寫編程語言的, 真正的目的是為了簡單。

前面提到我把這當作一個練手的項目, 為了熟悉整個過程, 我把簡單作為了整個過程的一個原則,很多地方我可以想到更優但更加複雜的實現方式, 但是大多數仍然採用了最簡單最能保持整個項目邏輯清晰的實現方式。 我更多的目的是為了瞭解整體過程, 整體結構, 所以局部就儘量保持簡單了吧(當然比較懶也是重要原因)。

當然後續詞法分析和語法分析肯定會自己實現一下, 畢竟這算是編譯器或者解釋器的前端, 也很重要的。

解釋器是用C語言寫的。 之前從來沒有用C語言寫過這麼大的項目(雖然到目前一共也就2千多行的代碼), 這次也讓我學會了很多C語言的高級用法。 比如 :

void (*func)(void)

是一個返回值為空, 參數為空的函數指針;

void (*signal(int signo,void(*func)(int)))(int);

是一個返回值為函數指針, 參數為(int signo,void(*func)(int)), (一個int, 一個函數指針)的函數, 其中函數名為signal。

之所以用函數指針, 是為了用C語言寫面向對象, 最開始我完全是使用面向過程, 只是簡單的通過不同文件實現簡單的封裝。

後來越寫越大, 就出現各種問題, 比如頭文件交叉引用引起編譯器報錯。 還有很多地方用面向對象可以更好地實現, 比如要處理表達式的創建和求值, 如果能有一個表達式的接口, 就能利用多態的好處, 不需要再寫一個巨大的switch, case語句, 使用枚舉來判斷不同的表達式, 調用不同的函數。

我聽說限制程序員的不是編程語言而是編程思維, C語言當然也可以寫面向對象, 數據可以封裝在結構體中, 再給結構體加上函數指針就實現了類的方法。

多態也可以通過自己實現虛函數表, 在對象初始化時把函數指針指向不同的函數就實現了。 大多數的面向對象的特性都有相應的方法實現, 只不過是語法上不如原生支持面向對象的語言簡單罷了。

還有一些問題是關於這門語言本身的設計問題:

(1) for, if這類的語句中變量的作用域問題

一開始我設計的是Java, C++一樣的, for, if的代碼塊中聲明的變量, 它作用域只存在於整個代碼塊中。 後來想到了這是一門弱類型的動態語言, 獨立的運行環境也沒什麼特別的用處, 於是就改成了和Python一樣: 這種代碼塊都沒有獨立的運行環境。

(2) 把函數看成什麼的問題

比如Java這種純粹面向對象的語言, 函數只能是對象的方法。 我這裡是把函數作為一種基礎數據類型, 像字符串一樣, 可以直接用於傳參, 賦值。

畢竟這是自己的編程語言,可以把它設計成自己喜歡的樣子, 所以大多數的設計都是根據自己的想法, 自己覺得怎麼合理就怎麼來(當然不是天馬行空地胡亂設計, 而是根據自己地實際經驗選擇合理的設計吧)。

當然最開始寫一門編程語言的時候,有很多地方不知道怎麼設計才合理, 這個時候我就參考自己學過的編程語言, 想想它為什麼要採用這種設計, 出於怎樣的考慮。

這樣的思考, 讓我對之前學過的編程語言有有了更加深刻的認識, 可以說是受益匪淺吧。 我漸漸地也認識到編程語言的設計很多時候都是設計者編程思維的體現。


簡單地介紹一下Hedgehog

說了這麼多, 是時候簡單地介紹一下我寫的這門編程語言了。目前還很簡陋, 後面再慢慢地完善它吧。

hedgehog 的多數設計和 python 比較相似, 無需聲明變量類型, if,for等語句沒有塊級作用域。

語法上又有點像 go 語言: if, for後面不需要(), 但是後面的代碼塊都必須加{};

沒有while, 不過有for condition {}來替代。 不過行尾必須加;這點和 go 不同。

大多數設計都是為了簡化實現方式, 比如必須加{}, ;是為了簡化語法的解析。


數據類型

a = 10; //int
b = 3.14;//float
c = true;//boolean
d = null;//null
s = "Hello, World!";//string


控制語句

a = 10;
if a > 10 { // `()` is not necessary.
b = a+20;
} elsif a==10 {
b = a+10;
} else {
b = a-10;
}
print(b);


循環

for i=0; i<10; i=i+1 {
print(i);
if i>=4 {break;}
}
i = 0;
for i<10 {
if i<5 {continue;}
print(i);
}


函數

function也被看作一種值(基本數據類型), 不過目前還沒有對它實現垃圾回收, 所以直接以函數賦值或者其他操作會出現內存錯誤。

// 模仿python首頁的函數
func fbi(n) {
a, b = 0, 1;
for a<n {
print(a);
a, b = b, a+b;//支持這種賦值方式
}
}
fbi(100);


func factorial(n) {
if n==0 {return 1;}
return n*factorial(n-1);
}
print(factorial(5));

目前只實現了一個原生函數print。 print接收一個基本數據類型作為參數, 輸出並換行, 或者無參數, 直接換行。


運算符

大多數與c保持一致, 除了&, |。 因為沒有提供位運算的功能, 所以直接用這兩個符號表示邏輯與和邏輯或。

b = 2;
a = 10;
if a>20 & b<10 {
print("`b` is less than 10 and `a` is greater than 20");
}
if a>20 | b<10 {
print("`b` is less than 10 or `a` is greater than 20");
}


What I cannot create, I do not understand。” 我喜歡這種從自己製作過程中學習的方式。 這種方式給了我一種踏實感, 讓我覺得自己是真地明白了整個過程,而不是僅僅記住了什麼公式, 學會了調用新的API。

這個簡單的解釋器的代碼都在我的GitHub上,感興趣的可以訪問下面的鏈接。

https://github.com/yangtau/hedgehog



原文出處:微信公眾號:碼農翻身

原作者: 楊韜

相關推薦

推薦中...