歡迎來到我們第 2 個主題月:Mechanical March。這個月我們把焦點放在系統程式語言,也就是會編譯成機器碼的語言。
這篇一部分是文章,一部分是 Mechanical March 影片的逐字稿。 我會先快速介紹這個月的主題,接著我們一起稍微看看系統程式語言、它們在歷史中的演變、編譯成機器碼的優缺點,最後再快速瀏覽每一種精選語言。 這次同樣有 Erik 和我一起,後半段大部分時間會由他來說。不過我先從一些實用資訊開始。
首先,是這個月的精選語言:C、C++、D、Go、Nim、Rust、V 和 Zig。想拿到 Mechanical March 徽章,你需要在其中一種語言裡完成 5 個練習。我們的 Go 學習軌道有 Exercism 上數一數二的課程大綱,真的很推薦你試試看。我們 Exercism 團隊也很喜歡 Nim,它算是相對容易上手、寫起來也很輕鬆的語言,所以我也很推薦你試試。
我們還有 5 個精選練習可以試試:
- linked-list 或 simple-linked-list(依語言而定):配置/釋放記憶體、指標
- secret-handshake:位元運算
- pangram:
for迴圈、字串與字元 - sieve:陣列、
for迴圈 - binary-search:陣列、迴圈
幾天前我在更新影片裡公布了一個新徽章,只要在主題語言中完成那 5 個精選練習就能得到。想拿到這個徽章,你得在今年之內用某種系統程式語言完成這些練習。
我們也正在醞釀很多有趣的內容:專訪 Go 核心團隊的幾位成員,也希望採訪到 Rust 和其他語言的開發者。這個月還會有很多直播。另外,Mechanical March 的周邊商品也快要推出了!
那我們就再深入一點,看看技術面的東西吧。
這些語言都用在什麼地方?
嗯,我們在 Exercism 各處都用得到它們。我們的 CLI 是用 Go 寫的,內部用來管理學習軌道的工具 configlet 是用 Nim 寫的,而負責計算你解答中程式碼行數的關鍵函式庫則是用 Rust 寫的。Erik,我們為什麼會為這些工具選擇這些語言?
CLI:
- 我想我們會用 Go,是因為那是 Katrina 最熟悉的語言。
- Go 很適合這類中小型的命令列工具。
- Go 的程式碼相對直截了當,讓人更容易參與貢獻
- Go 的執行檔很容易部署,因為它不需要執行環境
- Go 的交叉編譯做得很好
Nim
- Nim 有大部分和 Go 一樣的優點
- 我們沒有太多人懂 Go、能維護 CLI,所以就改用 Nim 了
你還會在哪些其他類型的場合看到這些語言?
- 任何效能很重要的地方(驅動程式、遊戲、作業系統、建置系統/編譯器)
- 任何資源有限的地方(例如嵌入式軟體)
- 任何需要高度可攜性的東西,也就是能在許多不同平台上執行
什麼是機器碼?
就像我前面說的,Mechanical March 裡的這些語言有個共同特點,就是會編譯成機器碼。可以請你稍微說明一下什麼是機器碼,以及相對的位元組碼又是什麼嗎?
- 機器碼是可以直接在機器上執行的程式碼。
- 相對地,位元組碼需要另一段程式碼來解讀,或把位元組碼編譯成機器碼。因此位元組碼在被執行之前,需要一個中介步驟。
比起位元組碼,機器碼有哪些優缺點?
優點:
- 啟動更快(不需要 JIT 編譯步驟)
- 佔用的記憶體更小(不用載入執行環境、記憶體中沒有位元組碼,對嵌入式系統特別有利)
- 目標機器不需要安裝執行環境(這對縮小 Docker 容器很重要)
缺點:
- 不可攜。位元組碼具有可攜性,但編譯出來的機器碼只適用於特定平台
- 無法進行進階的最佳化,例如設定檔導引最佳化(先讓程式跑一段時間,再決定怎麼編譯(位元組)碼最好)
注意:也可以採用混合做法,讓語言先編譯成位元組碼,再用另一套工具把位元組碼編譯成機器碼。
系統程式設計的演變
好,那我們來稍微看看這個月幾種語言的演變。首先,我們就從 C 開始,看看 C++ 是怎麼從它演變出來的。請你稍微談談這些語言。
C 是非常低階的語言。用起來的感覺就像只比機器碼高一點點。這讓它非常強大、也極容易最佳化,但同時也比較容易出現 bug(例如空指標例外和緩衝區溢位)。 記憶體管理全部得手動來,責任落在程式設計師身上,可能導致 bug 或記憶體洩漏。 C++ 就像 C,但加上了物件導向程式設計的支援。它還是相當低階,需要你自己手動管理記憶體。C 和 C++ 都允許你寫內嵌組合語言(ASM)!
那這些比較新的系統程式語言呢?它們又是怎麼演變的?
所有現代系統程式語言都支援自動記憶體管理,不論是透過參考計數、垃圾回收器,還是其他機制。
早期的系統程式語言都支援空指標(Tony Hoare 稱之為他的「十億美元失誤」)。空指標惡名昭彰,常常導致執行期錯誤和安全性漏洞。 許多現代語言乾脆捨棄 null,或者至少讓你得多費點功夫才能用到它。
另一個變化是,可變的值從預設變成例外:現在預設是不可變的值。例如 Rust 和 Vlang 的值預設都不可變,要可變得自己選擇加入。
所有較新的語言都支援與 C(或 C++)互通,因為已經有太多程式碼是用這些語言寫的。
還有一件有趣的事:有些較新的語言並不是直接編譯成機器碼,而是借助其他工具來完成。例如 Rust 和 Zig 使用 LLVM,而 Nim 則讓你選擇各式各樣的編譯器。這種做法稱為轉譯。
那巨集和超程式設計這類東西呢?
在巨集/超程式設計上,有個很有趣的分歧。C/C++ 的巨集很強大,但名聲有點微妙,大家都覺得不好駕馭。Rust、Nim 和 D 都提供強大的超程式設計能力,但方式優雅得多。反過來說,VLang 和 Zig 都明確把「沒有巨集」列為自家語言的特色,而 Go 則用 go generate 走出不同的路。
系統程式語言給人的印象是相當低階,這樣說還公平嗎?
較新的語言運作的抽象層次都比 C/C++ 更高。 例如 Rust、D 和 Nim 也允許用非常函式式的方式寫程式。 Nim 和 D 甚至還有「純」函式的概念。
本月語言簡介
接下來,我們就依序看看每一種語言。這些語言有很多相似之處:它們都是強型別、靜態型別。不過我們來看看它們有什麼不同。就從 C 開始好嗎?
C
- 由 Dennis Ritchie 開發
- 最古老、可能也是世界上使用最廣泛的語言之一
- 有大量軟體是用 C 寫的,例如 Unix 和 Linux
- 影響力極大(想想「類 C 語言」這件事就知道了)
- 手動記憶體管理
- 效能極高(貼近「硬體底層」)
- 到處都能跑
- 非常適合嵌入式系統
- 語言本身相當小
C++
- 由 Bjarne Stroustrup 開發
- C 的後繼者,但加入了物件導向(有類別的 C)
- 協助帶起物件導向程式設計的風潮
- 比 C 更多高階語言特性
- 透過樣板支援泛型程式設計
- 透過命名空間提供模組支援
- 許多遊戲(引擎)是用 C++ 寫的,Windows 也有一大部分是
- 手動記憶體管理
- 仍在持續演進,定期加入新特性(規格書很龐大)
D
- 由 Walter Bright 開發,後來 Andrei Alexandrescu 也加入
- 最初被視為重新設計過的 C++(從它的「錯誤」中學習),後來也從許多其他語言汲取靈感
- 多典範,支援指令式/物件導向/函式式程式設計
- 與 C/C++ 互通容易
- 統一函式呼叫語法
- 編譯期函式求值(例如在編譯期產生正規表達式的狀態機)
- 支援函式式程式設計與「純」函式
- 許多安全特性
- 透過 @safe 達成記憶體安全
- 契約(前置/後置條件、不變式)
- 純函式
- 重視單元測試,測試檔就放在被測原始碼旁邊(這在 Exercism 網站上還得開特例才行 :))
- Unicode
Rust
- 由 Mozilla Research 的員工 Graydon Hoare 開發,後來由 Mozilla 正式採用,現在屬於 Rust Foundation
- 多典範,支援物件導向(但很有主見,例如沒有繼承)、指令式與函式式程式設計(Option/Result 型別、模式比對)
- 許多新工具是用 Rust 寫的(例如 SWC,還有 Gleam,以及 Linux 核心支援的第二種語言;我們的程式碼行數計算器就靠它)
- 重視可靠性與效能
- 以 LLVM 為基礎
- 連續 7 年在 StackOverflow 調查中獲選為最受喜愛的語言
- 速度快,部分原因是核心與標準函式庫都很精簡
- 安全:透過所有權與生命週期同時做到記憶體安全與執行緒安全,預設不可變
- 強大的型別系統,能在編譯期攔下許多 bug(尤其是記憶體相關的)。編譯器的錯誤訊息非常有用
- 該有的都內建:編譯器、建置工具、格式化工具、套件管理員、IDE 整合
- 文件寫得很棒(甚至有一份說明編譯器如何運作的龐大文件)
- 可攜:編譯成單一靜態執行檔,不需要安裝執行環境
- 與 C 程式碼互通容易
- 零成本抽象
- 無畏並行
- 巨集
Nim
- 由 Andreas Rumpf 開發(最初叫 Nimrod)
- 語法受 Python 啟發
- 多典範
- 在 Exercism 的 configlet 中使用
- 效能優異:零負擔的疊代器、以值為基礎的型別偏好使用堆疊配置
- 現代且富表達力的型別系統:型別推論、元組、泛型、總和型別、async/await
- 有垃圾回收,但也支援確定性的記憶體管理(多種記憶體管理選項)
- 編譯期執行程式碼
- 統一呼叫語法
- 巨集:很容易擴充語言
- 效應系統:把副作用編碼進型別系統中
Go
- 由 Google 的 Robert Griesemer、Rob Pike 和 Ken Thompson 開發
- 主要是指令式/程序式,支援類似物件導向的做法(但沒有繼承)
- 用於 Docker、Kubernetes 等大型專案。也很適合後端與 CLI(例如 esbuild)
- 目標是簡單到能裝進腦子裡(語法很少)
- 透過垃圾回收器達成記憶體安全
- 快:編譯快、測試快、執行也快。內建支援撰寫基準測試!
- 很有主見:花了很多心力去引導 Go 程式碼的風格,包括語法少、用 go fmt 格式化程式碼、提供工具檢查程式碼是否慣用、對其他語言只是警告的事情直接報錯(例如未使用的變數)、用文件列出該遵循的 Go 慣用寫法
- 輕量的型別系統讓 Go 非常有彈性(支援型別推論)
- 可攜:編譯成單一靜態執行檔,不需要安裝執行環境。交叉編譯很容易。並行則透過 goroutine(輕量)達成,並透過 channel 溝通
- 透過介面實現結構型別(類似鴨子型別,但會做靜態檢查)
- 錯誤處理:語言鼓勵你檢查並處理錯誤
VLang
- 由 Alexander Medvednikov 和 Delyan Angelov 開發
- 受 Go 啟發:
- 同樣抱持「事情只有一種做法」的策略
- 同樣抱持「語法極簡」的策略
- 協程
- 和 Go 不同的地方:
- 沒有 nil/null,而是使用 result 型別
- 預設不可變
- 總和型別(函式式)
- 字串插值
- 更小的執行環境與執行檔
- 模式比對
- 正在努力讓垃圾回收變成選用(autofree)
- 零成本的 C 互通
- 從程式碼產生文件
- 編譯器快,佔用的記憶體也少
Zig
- 由 Andrew Kelley 開發
- 語法相當少(500 行的 PEG 文法檔)
- 編譯期執行程式碼與反射
- 目標是「顯而易見」,沒有隱藏的控制流程、配置或巨集/超程式設計
- 手動記憶體配置
- 支援不同的配置器
- 標準函式庫中會配置記憶體的函式都有一個配置器參數
- 測試框架能偵測記憶體洩漏
- 安全:
- 錯誤是值,必須被處理
- 沒有 null,使用可選型別
- 測試可以和原始碼寫在同一個檔案(像 D 一樣)
- 使用 LLVM 作為後端
- 可以編譯 C/C++ 程式碼
- 與 C 互通容易
- 交叉編譯容易
結語
我們就先講到這裡,我想大家到這個階段腦袋應該都裝得很滿了。
希望這是一場實用又有趣的介紹,帶你認識這個月的精選語言。希望你探索這些語言的過程玩得開心。Erik 和我都很想知道你選了哪幾種、覺得如何,歡迎在留言區或論壇上分享!
感謝收看!