iHiD 上傳的大頭貼

Summer of Sexps 來了!

@iHiD
3年多前
影片

簡介

嗨,大家好!希望大家一切都好。

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 這類語言:語法精簡、語意單純,卻又非常有表達力。

2023年06月01日 · 覺得有幫助嗎?