6 個程式設計範型將改變你對程式設計的看法

發表於2017-05-13

每時每刻我都在琢磨一種程式語言所做的一些與眾不同的事情,這改變了我對程式設計的思考。在這篇文章中,我想分享一些我最喜歡的發現。

這不是那種“函數語言程式設計將改變世界”的部落格文章:這篇文章的內容會更加深奧。我敢打賭大多數讀者都沒有聽過下面的程式語言和範型,所以我希望你像我一樣有很大的興趣來學習這些新概念。

注意:對於下面的大多數語言我擁有的經驗很少:我只是發現它們背後的思想十分有魅力,但對於它們我沒有任何專業知識,所以有任何更正和錯誤請指出。另外,如果你發現這裡存在沒有提到的任何新的範型和想法,歡迎把它們分享出來。

更新:這篇文章上了 r/programming 和 HN 的首頁。感謝反饋,我已經新增了一些修正。

預設支援併發(Concurrent by default)

示例語言:ANIPlaid

讓我們先從改變思維方式開始吧:有一些程式語言預設情況下就是支援併發的。也就是說,每行程式碼都是並行執行的。

例如,假設你寫了三行程式碼 A,B 和 C:

在大多數程式語言中,A 會先執行,然後執行 B,最後執行 C。但在像 ANI 這樣的語言中,A, B, 和 C 都將同時執行。

ANI 語言中程式碼行之間的控制流或排序只是程式碼行之間顯式依賴的副作用。例如,如果 B 具有對 A 中定義的變數的引用,則 A 和 C 將同時執行,並且 B 將在 A 完成之後執行。

來看一下 ANI 中的一個例子。如教程中所述,ANI 程式由用於操作流和資料流的“管道”和“鎖存器”組成。這種非同一般的語法很難解析,而且這門語言似乎已經死了,不過這些概念還是非常有趣的。

這是 ANI 中的“Hello World”示例:

在 ANI 語法中,我們將 "Hello, World!" 物件(一個字串)傳送到 std.out 流。如果我們傳送另一個字串到 std.out 會怎麼樣?

這兩行程式碼並行執行,所以它們可能在控制檯以任何順序結束。現在,看看當我們某行程式碼中引入一個變數並在之後引用會發生什麼情況:

第一行宣告瞭一個名為 s 的 “鎖存器”(鎖存器有一點像變數),其中包含一個字串;第二行將 "Hello, World!" 傳送到 s,第三行“解鎖” s 並將內容傳送到 std.out。因此,可以看到 ANI 的隱形程式排序:由於每一行的執行都取決於前一行,因此這裡的程式碼將會按照編寫的順序執行。

Plaid 語言也聲稱預設情況下支援併發,但使用的是這篇論文中所描述的一種許可權模型來構建控制流。Plaid 還探索了其他有趣的概念,例如 Typestate-Oriented Programming(面向型別狀態程式設計),其中狀態更改成為語言的重要因素:你定義的物件不再是作為類,而是可以由編譯器檢查的一系列狀態和轉換。看起來這十分有趣,正如 Rich Hickey 的 Are we there yet 講話中所討論的將時間作為語言結構的首要因素。

Multicore 正處在上升期,併發性仍然比大多數語言更難。ANI 和 Plaid 對於這個可能產生驚人的效能提升的問題提供了一個新的思路;不過問題是“預設支援並行”是否讓併發更容易或難以管理。

更新:上面的描述講解了 ANI 和 Plaid 的基本本質,但我互換地使用了術語“併發”和“並行”,事實上它們具有不同的含義。有關更多資訊,請參閱 Concurrency Is Not Parallelism (併發不是並行)這篇文章。

 

依賴型別(Dependent types)

示例語言:IdrisAgdaCoq

你可能已經習慣 C 和 Java 等語言的型別系統,編譯器可以檢查一個變數是整數、列表還是字串。但如果你的編譯器可以檢查變數是“正整數”、“長度為 2  的列表”,還是“一個迴文字串”,那又會怎麼樣呢?

這是支援依賴型別的語言背後的思想:你可以在編譯時指定檢查變數值的型別。Scala 的 shapeless 庫增加了對 Scala 依賴型別的部分實驗支援(尚未正式支援),並提供了簡單的方法來檢視示例。

這裡是如何宣告一個 Vector 的程式碼,其中使用了 shapeless 庫,包含值 1,2,3:

這裡建立了一個變數 l1,它的型別簽名不僅指定它是一個包含 Ints 的 Vector,而且還指定它是一個長度為 3 的 Vector。編譯器可以使用此資訊來捕獲錯誤。讓我們使用 Vector 中的 vAdd 方法來執行兩個 Vectors之間的成對加法(pairwise addition):

上面的例子正常執行,因為型別系統知道兩個 Vectors 的長度都為 3。然而,如果我們嘗試 vAdd 兩個長度不同的 Vectors,我們會在編譯時得到一個錯誤,而不必等到執行時。

Shapeless 是一個了不起的庫,但就我所看到的,它仍然有點粗糙,只支援依賴型別的一個子集,並導致生成相當詳細的程式碼和型別簽名。另一方面,Idris 使型別成為程式語言的首要成員,因此它的依賴型別系統看起來更強大和更乾淨。為了比較,請檢視演講 — Scala vs Idris: Dependent Types, Now and in the Future 。

形式化驗證方法已經存在很長一段時間了,但大多數情況下都十分麻煩,無法用於通用程式設計。依賴型別在像 Idris 這樣的語言中,甚至在未來的 Scala 中,可能會提供更輕量的和更實用的替代方案,這仍然能大大提高型別系統提供捕獲錯誤的能力。當然,沒有型別系統可以捕獲所有的錯誤,由於終止(halting)問題的固有侷限性,但如果做得好,依賴型別可能是靜態型別系統下一個大的飛躍。

 

Concatenative 語言

示例語言:Forthcatjoy

有沒有想過在沒有變數和函式應用的情況下程式設計是一種怎樣的體驗?沒有?我也沒試過。但顯然有些人做了,他們提出了 concatenative 程式設計這個概念。這個概念背後的思想是語言中的所有內容都是一個函式,用於將資料推送到堆疊或從堆疊彈出資料;程式幾乎完全通過功能組合來構建(concatenation is composition)。簡單的說即是基於堆疊的程式語言。

這聽起來很抽象,所以讓我們來看一個簡單的例子:

在這裡,我們將兩個數字推到堆疊上,然後呼叫 + 函式,它將兩個數字從堆疊中彈出,並將結果新增到堆疊中:程式碼的輸出是 5。這裡有一個更有趣的例子:

我們來一行一行看這段程式碼:

  1. 首先,我們宣告一個函式 foo。請注意,cat 中的函式不指定輸入引數:所有引數從堆疊中隱式讀取。
  2. foo 呼叫 < 函式,它會彈出堆疊中的第一個選項,將其與 10 進行比較,並將 True 或 False 返回到堆疊。
  3. 接下來,我們將值 0 和 42 推到堆疊上:我們將它們放在括號中,以確保它們被壓入未被評估的堆疊。這是因為它們將被用作“then”和“else”分支(分別)用於呼叫下一行的 if 函式。
  4. if 函式從堆疊中彈出 3 個選項:布林條件,“then”分支和“else”分支。根據布林條件的值,它會將“then”或“else”分支的結果推回堆疊。
  5. 最後,我們將 20 推到堆疊並呼叫 foo 函式。
  6. 當所有都完成了,我們最終會得到數字 42。

有關這種語言更詳細的介紹,請參閱 The Joy of Concatenative Languages

這種程式設計風格有一些有趣的屬性:

  • 程式可通過無數的方式來分割和連線,以建立新的程式;
  • 極簡的語法(甚至比 LISP 還小)產生了非常簡潔的程式;
  • 強大的超程式設計支援

我發現 concatenative 程式設計是一個非常開眼界的體驗,但我還沒實踐過。似乎你必須記住或想象堆疊的當前狀態,而不是能夠從程式碼中的變數名讀取它,這可能使得很難理解程式碼。

 

宣告式程式設計(Declarative programming)

示例語言:PrologSQL

宣告式程式設計已經存在了很多年,但大多數程式設計師仍不知道它是一個怎樣的概念。簡要來說:在大多數主流語言中,你是在描述如何解決特定的問題;在宣告式語言中,你只需描述所需的結果,語言本身可推匯出結果。

例如,如果你在 C 語言中從頭開始編寫排序演算法,你會編寫合併排序的說明,逐步描述如何遞迴地將資料集分為兩部分並按順序將其合併到一起:這裡是一個例子。如果使用宣告式語言如 Prolog 對數字進行排序,可以直接描述你需要的輸出:“我想要相同的值列表,但索引 i 中的每個專案應小於或等於索引 i + 1 中的專案”。將上面 C 語言的解決方案和 Prolog 程式碼進行比較:

如果你使用過 SQL,那麼你已經使用了宣告式程式設計,可能自己沒有意識到這一點:當你發出一個像 select X from Y where Z 這樣的查詢,你就是在描述你想要返回的資料集;資料庫引擎的工作實際上是如何執行查詢。你可以在大多數資料庫中使用 explain 命令來檢視執行計劃並弄清楚在引擎下發生了什麼。

宣告式語言的優點在於它允許你在更高層次的抽象下工作:你的任務就是描述所需輸出的規範。例如,在 Prolog 語言實現的一個簡單數獨解算器中只列出了一個數獨謎題的答案的每行、列和對角線應該是怎樣的:

下面是如何執行上面的數獨解算器:

不幸的是,宣告式程式語言的缺點是效能開銷大。上面提到的排序演算法時間複雜度可能是 O(n!);數獨解算器使用暴力搜尋;而且大多數開發人員不得不提供資料庫提示和額外的索引,以避免執行 SQL 查詢時開銷大且效率低的計劃。

 

符號式程式設計(Symbolic programming)

示例語言:Aurora

Aurora 語言是符號式程式設計的一個例子:使用這些語言編寫的“程式碼”不僅可以包含純文字,還可以包括影像、數學方程、圖和圖表等。你可以以該資料的原生格式來操作和描述各種大量的資料,而不是全部以文字形式描述。Aurora 也是完全可互動的,它會立即顯示每行程式碼的結果,像 steroids 中的 REPL。

Aurora 語言由 Chris Granger 建立,它還構建了 Light Table IDE。Chris 在它的文章 Toward a better programming(為了更好地程式設計)中概述了建立 Aurora 的動機:一些目標是使程式設計更直觀、直接和減少偶然的複雜性。要了解更多的資訊,觀看 Bret Victor 的演講:Inventing on PrincipleMedia for Thinking the Unthinkable, 和 Learnable Programming

更新:“符號式程式設計”可能不太適合用於描述 Aurora。有關更多資訊,請參閱 Symbolic programming 的維基主頁。

 

基於知識的程式設計(Knowledge-based programming)

示例:Wolfram Language

很像上面提到的 Aurora 語言,Wolfram 語言也是基於符號的程式設計。然而,符號層僅僅是為 Wolfram 語言的核心提供一致的介面,Wolfram 語言是基於知識的程式語言:內建了大量的庫、演算法和資料。這使得可以輕鬆地從圖形化的 Facebook 連線,到操縱影像、查詢天氣、處理自然語言查詢、繪製地圖上的方向、求解數學方式等方面做好一切。

我猜想 Wolfram 語言有最大的“標準庫”和任何現有語言的資料集。對於 Internet connectivity 是編寫程式碼的固有功能,我也感到十分興奮:它幾乎像一個 IDE,其中的自動完成功能進行谷歌搜尋。看符號式程式設計模型是否像 Wolfram 所說的那樣靈活並真正利用所有這些資料,這將是非常有趣的。

更新:儘管 Wolfram 聲稱 Wolfram 語言支援“符號式程式設計”和“知識程式設計”,但這些術語的定義有所不同,有關更詳細的資訊請參閱 Knowledge level 和 Symbolic Programming 的維基主頁。

相關文章