簡介
嗨,大家好!希望大家一切都好。
Exercism 最近幾週真的非常精彩,我們推出了 Exercism Premium 和 Exercism Insiders。我們也辦了幾場很棒的社群線上會議,網站上有很多改進已經上線,還有更多即將推出。現在有很多值得興奮的事,但最令人興奮的,莫過於我們要邁入 #12in23 的第 6 個月了!也就是「S 表達式之夏」,說得順口一點就是「Sexps 之夏」。
一如往常,和我一起的是程式世界的智者 Erik。
這個月我們有五種語言:Clojure、Common Lisp、Emacs Lisp、Racket 和 Scheme。這些語言都是 Lisp 的方言,所以這支影片不會太深入個別語言,我們會多花點時間談談 Lisp 本身,以及它獨特的地方。最後再簡單介紹一下這幾種語言。
不過在那之前,先來說些例行事項!想拿到「Sexps 之夏」徽章,你必須在六月期間,完成其中任一種語言裡的任意五個練習。
徽章
另外還有貫穿整年的 12in23 徽章。想拿到它,你需要在同一種語言裡解完五個精選練習。如果你是六月之後才看到這支影片,這部分在整年任何時候都能完成,所以不用擔心錯過。因為很多人可能從沒碰過 Lisp,我們盡量挑選比較簡單的練習,讓你嘗嘗 Lisp 語言是什麼滋味。
- **閏年:**練習布林條件與真值判斷(也可以再加上詞法作用域)
- **買一送一:**格式化字串,並練習使用選擇性參數
- **平方差:**呼叫自訂函式,並用前綴表示法做數學運算
- **機器人名稱:**處理隨機性、原子和結構化資料
- **括號配對:**用遞迴驗證字串
這些練習以及前幾個月的練習,都可以在 #12in23 頁面上找到。
概觀
那麼,來談談以 Lisp 為基礎的語言吧。我想,我們應該先稍微了解 Lisp 本身。就先從 Lisp 的整體介紹開始。
Lisp
-
首先要注意的是,Lisp 是最古老的語言之一。
-
它由 John McCarthy 於 1958 年在 MIT 創造,那時候電腦還會從頭到腳占滿整個房間 🙂
-
Lisp 這個名字代表 LISt Processing(也就是 LISt Processor),突顯了陣列這種資料結構的重要性。
-
它的設計目的是為了進行 AI 研究。
-
Lisp 以 Lambda 演算為基礎,那是 Alonzo Church 發明的形式系統,用來(簡化地來說)描述數學中的計算。
-
它是一門極具影響力的語言,原因有很多:
-
它是目前仍廣泛使用的第二古老高階程式語言(僅次於 Fortran)
-
它是第一個高階函數式程式語言,並引入了許多我們現在會和函數式程式設計聯想在一起的特性。
-
不過要注意,Lisp 仍然支援命令式程式設計
-
它是第一個內建垃圾回收的語言,讓開發者不必自己手動管理記憶體
-
它的語法相對精簡,語意也相對單純,因此非常適合用來教學。
-
所以 Lisp(更精確地說,是它的某個方言)經常被用來教程式設計
-
它衍生出(而且還在持續衍生!)非常多語言方言,我們接下來會談談 Exercism 上有支援的那些。
-
換句話說,在程式語言的樹狀圖裡,Lisp 這類語言自成一枝,就像 C 這類語言也自成一枝一樣。
我最近和 Haskell 的創造者之一 Simon Peyton Jones 聊過,他談到以圖靈機為基礎的語言,和以 Lambda 演算為基礎的語言之間的差異。如果你想多了解這方面,很值得去看看那場訪談。
括號
Lisp 裡有很多括號,但這不一定是壞事(就像 C 這類語言裡有很多大括號,也不見得是壞事一樣)。 Lisp 的核心圍繞著一種叫做 S 表達式的東西。 S 表達式(symbolic expression 的簡稱,縮寫為 sexpr 或 sexp,這也是本月挑戰名稱的由來)是一種用來表示資料的運算式。它是為了最初的 Lisp 語言而發明,並由 Lisp 發揚光大。 S 表達式有兩種形式:
- 原子(例如 x)。可以把它們想成不巢狀的「值」,或是樹狀結構裡的葉節點
- 形式為 x . y 的運算式,其中 x 和 y 都是 S 表達式。可以把它們想成配對,y 是陣列中的下一個元素(如果有的話),或是樹狀結構裡的節點。注意這是遞迴定義,會在葉節點那一層結束。這種 S 表達式通常會用括號來表示。
S 表達式
S 表達式在 Lisp 裡同時用來表示資料和陣列。 所以每次你定義一個陣列,都會用到括號。 再加上以下幾點: 陣列是 Lisp 的核心資料結構(名字就是這麼來的), 在某些 Lisp 方言裡,它甚至是唯一的資料結構, 結果就是括號多到不行。 要說明陣列有多核心,看看這個例子:在 Lisp 裡想呼叫一個函式,你得建立一個陣列。
有趣的是,陣列的第一個元素(也就是 head)代表被呼叫的函式,其餘元素(也就是 tail)則當作引數傳進去。 這叫做前綴表示法(運算子寫在運算元前面),一開始可能會覺得有點怪,但其實非常實用:
- 你可以把運算子套用到多個引數上,不必重複寫出運算子(例如 (+ 1 2 3))
- 運算子的優先順序變得明確,因為要呼叫不同的運算子,本來就得另外寫一個 S 表達式
有趣的是,陣列甚至可以用來表示原始碼,這點我們待會再回來談。
一般來說,多數 Lisp 方言的語法相當精簡,語意也相對單純,因此比較容易學習,程式碼也更容易理解。 語法精簡並不代表功能就比較弱! 這兩件事加起來(精簡的語法 + 單純的語意),讓 Lisp 很適合用來寫編譯器和翻譯員。 如果你哪天想自己寫一個編譯器,動手做一個 Lisp 是個不錯的選擇!
Lisp 的酷功能
前面提過,Lisp 在內部用相同的資料型態和資料結構來表示程式碼。 這種特性叫做同像性。 換句話說,如果一門語言寫出的程式可以用該語言當成資料來操作,因而只要閱讀程式本身就能推斷它的內部表示,那麼這門語言就具有同像性。 這個特性常被濃縮成一句話:這種語言把程式碼當成資料看待。
語言
Scheme
- 由 Guy Steele 和 Gerald Sussman 於 1970 年代在 MIT AI Lab 創造。
- 最初是為了透過一個極小的 Lisp 翻譯員來理解 Carl Hewitt 的 Actor 模型。
- 這門語言本身是在一系列 AI Memo 研究文件中發表的,這些文件後來統稱為 Lambda Papers。
- 第一個採用詞法作用域的 Lisp 方言(值只在自己被定義的作用域裡有效),也是最早支援一級續延的語言之一。
- 有官方的 IEEE 標準,也有一套實質標準,叫做《Revised Report on the Algorithmic Language Scheme》(RnRS)。
- 實作很多:ChezScheme、Guile(兩者 Exercism 都有支援)、MIT/GNU Scheme 和 Racket
- 語言極度精簡、語法很少,但這並非刻意設計的結果。
- 作者們原本想做出複雜的東西,最後卻設計出比他們預期簡單得多的語言
- 具備正規的尾端遞迴。用遞迴來做疊代,是最道地的寫法。
- Scheme 會最佳化尾端遞迴呼叫,讓它不佔用堆疊空間或其他資源。這表示遞迴可以用在任意大的資料,或任意長的計算上
- 強大的數值資料型態,包括有理數和複數
- 延遲求值,概念類似 promise。
- 強大的巨集系統。
- 衛生巨集能降低定義巨集時出現非預期結果的可能性。
Common Lisp
- Common Lisp 的開發始於 1981 年,由 ARPA 的主管 Bob Engelmore 發起,目標是制定單一的社群標準 Lisp 方言,因為當時使用的各種方言往往彼此不相容,導致程式碼與知識無法共享
- 第一版標準於 1984 年發布,最終版則在 1994 年發布(規格非常穩定)
- 由於是一套標準,因此有各種不同的實作,例如 Steel Bank Common Lisp(Exercism 的預設實作)和 CLisp。
- 也有商業實作,例如 Allegro CL 和 LispWorks,另外還有可以嵌入 C 程式的 ECL(Embeddable Common Lisp),以及執行在 Java 虛擬機器上的 ABCL。
- 由標準定義(ANSI INCITS 226-1994),所以 30 年前寫的程式碼今天執行起來依然沒問題
- 豐富且可擴充的型別系統
- 為 image 和 REPL 開發而設計,因此非常容易內省。
Emacs Lisp
- 開發於 1985 年,目的是要有一門高效率的語言來擴充文字編輯器
- 動態型別
- Emacs 大約 80% 是用 Emacs Lisp 寫的(另外 20% 為了效能而用 C 寫)
- 和其他 Lisp 有點不同:
- 沒有標準化,仍在緩慢演進
- 沒有自動的尾端呼叫消除,改透過 named-let 巨集支援(會轉換成 while 迴圈)
- 預設是動態作用域,新寫的程式碼則建議採用詞法作用域
- 編輯器內建良好的說明文件
- 跨平台(只要 Emacs 能跑的地方就能跑)
- 可以透過閱讀你每天使用功能的程式碼來學這門語言(Emacs 核心 + 各種套件)
- 透過 cl-lib 套件可以使用一部分的 Common Lisp。Emacs Lisp 相當精簡,Common Lisp 的功能則完整得多,cl-lib 套件就是把 CL 的一部分帶進來。
Racket
- Matthias Felleisen 創立了 PLT Inc.,該公司在 1995 年 1 月決定開發一套以 Scheme 為基礎的教學用程式設計環境。最初名為 PLT Scheme,後來改名為 Racket。
- 除了是教學用的程式設計環境,它也被設計成程式語言設計與實作的平台。
- 現代化的 LISP,Scheme 的後裔
- 支援邏輯程式設計!
- 語法簡單、表達力強,既適合初學者,在專家手裡也威力十足
- 支援多種程式設計範式:函數式程式設計、物件導向程式設計、契約式設計、邏輯程式設計、超程式設計
- 完整的標準函式庫
- 內建 DrRacket,這是一套完整的 IDE,專為學習與探索而設計,用起來幾乎不費力
- 說明文件非常優秀,有大量背景知識和範例
Clojure
- 由 Rich Hickey 開發,目標是打造一門能在 JVM 上執行、並具備優異並行能力的現代 LISP
- 是 LISP 的方言,但和其他 LISP 有些不同:它不支援隱含的尾端遞迴(不知道這是什麼也別擔心),而且資料結構不只陣列,還有 map、set、vector。這些資料結構都有各自的字面語法。
- 它們也全都是不可變的,但查詢效能依然很好,O(log32 n) 的查找可說是「實質上」的常數時間
- 透過 multimethod 和 protocol 實現執行期多型
- 與 JVM 的互通性極佳
- Clojure Spec 資料規格系統(屬於執行期,而非編譯期),讓你可以定義資料的結構、產生資料、進行基於屬性的測試等等
應用場景
Scheme
- 用於教育,協助教授資訊科學(極具影響力的《Structure and Interpretation of Computer Programs》就是用 Scheme)。
- 用於 AI。也當作腳本語言使用,例如在 GIMP(圖形編輯器)、CAD 工具(電腦輔助設計)中,甚至用在電影上,例如《Final Fantasy: The Spirit Within》的算繪引擎管理腳本
Common Lisp
- Common Lisp 的應用範圍很廣,例如人工智慧與研究領域,也有商業應用:NASA 用 Common Lisp 寫了 Deep Space One 太空船的自動駕駛軟體,Viaweb 也是用 Common Lisp 寫的,後來被 Yahoo 收購並更名為 Yahoo Store!,另外還有第一版的 Reddit
Emacs Lisp
- Emacs Lisp 用在哪裡呢?就用在 Emacs 裡!
- 基本上,Emacs 就是 Emacs Lisp 的翻譯員。Emacs Lisp 是 Lisp 程式語言的方言,但額外加入了支援文字編輯的擴充功能
Racket
- 用於教育,因為 Racket 的設計重點就是支援語言的建立、簡化與分析。
- 用於研究,因為它可擴充的語法與語意,適合用來設計新語言與語言功能,並製作原型。
- 用於遊戲,例如以《Doom》聞名的 John Carmack 用它打造 VR 的互動式腳本環境,開發商 Naughty Dog 也用它來寫腳本(例如《Uncharted》)。Hacker News 是用 Arc 寫的,Arc 也是 Lisp,而它本身則是用 Racket 寫的。
Clojure
- Clojure 的用途很多,例如 Docker 在 2022 年收購了 Atomist,那是一套用 Clojure 實作的容器安全與自動化平台。
- 全球最大的 Clojure 使用者是 Nubank,這家新興銀行幾年前收購了它,現在還雇用整個 Clojure 核心團隊。
- 由於它動態且互動性高,因此大量用於快速製作原型。
程式設計的角度
這些語言都支援函數式、命令式和符號式範式。 其中有些也支援 OOP,最著名的是 Common Lisp。
Lisp 大多是動態語言,不過 Racket 支援靜態型別。
這不代表它們全都是靠翻譯員執行的,實際上是混搭的:有的直接由翻譯員執行(完全不經過編譯步驟),有的是先編譯成位元組碼再交給翻譯員執行,也有直接編譯成機器碼的選項。
Scheme
- 極簡,語意清楚單純,組成運算式的方式很少。
- 讓語言容易學習、程式碼容易理解。
- 因此,Scheme 也常被用在許多資訊科學的入門課程中
- 一級續延。
- 續延是程式狀態的一種表示。
- 續延可以用來模型化控制流程(例如
return這樣的結構),或是協程(讓程式能進行多工)
Common Lisp
- 可擴充的物件導向系統,具備可程式化的方法組合(既包含子類別與父類別方法的組合方式,也包含 before、after 和 around 方法,讓系統能在不需修改的情況下被擴充)
- 可程式化的條件系統(可說是「例外」的超集),能把條件的辨識與處理方式的選擇解耦。條件系統比例外系統更有彈性,因為它不把責任分成兩半,也就是發出錯誤1的程式碼和處理它2的程式碼,而是拆成三個部分:發出條件、處理條件,以及重新啟動。
- 巨集可以擴充語言的語法,不只是產生樣板程式碼。這能幫助你打造適合特定領域的語言,而不是反過來遷就語言。
Emacs Lisp
- 編輯器支援與整合非常好
- 可以在 Emacs 執行中自訂 Emacs(「就像替自己做腦部手術」:))
- 可以用批次模式執行,在這種模式下,編輯器處理文字的能力全都任你使用(例如 buffer 和移動指令)
Racket
- 強大的巨集系統,像 threading 巨集這類語法糖就建立在這之上。巨集也具備衛生性,這正好回答了一個簡單的問題:巨集產生出來的程式碼會放到別的地方,當那些程式碼被求值時,我們該如何決定其中識別字的綁定?衛生巨集能降低定義巨集時出現非預期結果的可能性。
- 以語言為導向。
- Racket 內建各種工具,讓你可以在 Racket 巨集之上,寫出自己的程式語言或 DSL。
- 內建多種語言,例如 Typed Racket(支援靜態檢查的型別標註)、datalog(類似 Prolog 的語言,DrRacket IDE 有支援),以及 Scribble,一套用來建立 HTML 或 PDF 形式文件的工具
- REPL 是開發工作流程的核心,不只是拿來試東西或查文件用的
Clojure
- 強大的巨集系統。
- 像 threading 巨集這類語法糖就建立在這之上
- REPL 是開發工作流程的核心,不只是拿來試東西或查文件用的
該試哪一種
- 如果你從沒碰過 Lisp,Scheme 和 Racket 是很好的選擇,兩者的語法都非常精簡。
- 話說回來,Common Lisp 和 Clojure 都有學習模式,所以在 Exercism 上大概是最適合入門的選擇。
- 如果你本來就在用 Emacs,Emacs Lisp 自然是首選。
- 同樣地,如果你用的是 JVM 語言,Clojure 是很自然的選擇。
- Emacs Lisp(透過 Emacs)、Clojure(透過 IntelliJ)和 Racket(透過 DrRacket)都有很棒的 IDE 支援。
- 當然,Common Lisp 和 Scheme 也都有不錯的 IDE。
- 如果你想要功能非常完整的 Lisp,Common Lisp、Clojure 和 Racket 都相當豐富
- 如果你想要有點不一樣的 Lisp,Clojure 有一套對 Lisp 來說相當獨特的語法。
- 如果你對巨集和超程式設計有興趣,基本上它們都是好選擇!但如果你想打造新語言,Racket 尤其出色
當然,如果你有時間,我會建議你挑兩三種試試看! 另外,別害怕括號!我以前也很怕,所以才會拖了好久才學 Lisp。 不過你很快就會習慣它們,說不定還會像我一樣開始欣賞它們喔。 事實上,我現在很喜歡 Lisp 這類語言:語法精簡、語意單純,卻又非常有表達力。