摘要: 文章介紹了一些比較小眾的編程范型,也提到了一些小眾的語言,作者希望借此讓大家更多的了解一些不是主流的編程范型,進而改變對編程的看法和思考。
每時每刻我都在琢磨一種編程語言所做的一些與眾不同的事情,這改變了我對編程的思考。在這篇文章中,我想分享一些我最喜歡的發現。
這不是那種“函數式編程將改變世界”的博客文章:這篇文章的內容會更加深奧。我敢打賭大多數讀者都沒有聽過下面的編程語言和范型,所以我希望你像我一樣有很大的興趣來學習這些新概念。
注意:對於下面的大多數語言我擁有的經驗很少:我只是發現它們背后的思想十分有魅力,但對於它們我沒有任何專業知識,所以有任何更正和錯誤請指出。另外,如果你發現這里存在沒有提到的任何新的范型和想法,歡迎把它們分享出來。
更新:這篇文章上了 r/programming 和 HN 的首頁。感謝反饋,我已經添加了一些修正。
默認支持並發(Concurrent by default)
讓我們先從改變思維方式開始吧:有一些編程語言默認情況下就是支持並發的。也就是說,每行代碼都是並行執行的。
例如,假設你寫了三行代碼 A,B 和 C:
A;
B;
C;
在大多數編程語言中,A 會先執行,然后執行 B,最后執行 C。但在像 ANI 這樣的語言中,A, B, 和 C 都將同時執行。
ANI 語言中代碼行之間的控制流或排序只是代碼行之間顯式依賴的副作用。例如,如果 B 具有對 A 中定義的變量的引用,則 A 和 C 將同時執行,並且 B 將在 A 完成之后執行。
來看一下 ANI 中的一個例子。如教程中所述,ANI 程序由用於操作流和數據流的“管道”和“鎖存器”組成。這種非同一般的語法很難解析,而且這門語言似乎已經死了,不過這些概念還是非常有趣的。
這是 ANI 中的“Hello World”示例:
"Hello, World!" ->std.out
在 ANI 語法中,我們將 "Hello, World!"
對象(一個字符串)發送到 std.out
流。如果我們發送另一個字符串到 std.out
會怎么樣?
"Hello, World!" ->std.out "Goodbye, World!" ->std.out
這兩行代碼並行執行,所以它們可能在控制台以任何順序結束。現在,看看當我們某行代碼中引入一個變量並在之后引用會發生什么情況:
s = [string\]; "Hello, World!" ->s; \s ->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)
你可能已經習慣 C 和 Java 等語言的類型系統,編譯器可以檢查一個變量是整數、列表還是字符串。但如果你的編譯器可以檢查變量是“正整數”、“長度為 2 的列表”,還是“一個回文字符串”,那又會怎么樣呢?
這是支持依賴類型的語言背后的思想:你可以在編譯時指定檢查變量值的類型。Scala 的 shapeless 庫增加了對 Scala 依賴類型的部分實驗支持(尚未正式支持),並提供了簡單的方法來查看示例。
這里是如何聲明一個 Vector
的代碼,其中使用了 shapeless 庫,包含值 1,2,3:
val l1 = 1 :#: 2 :#: 3 :#: VNil
這里創建了一個變量 l1
,它的類型簽名不僅指定它是一個包含 Ints
的 Vector
,而且還指定它是一個長度為 3 的 Vector
。編譯器可以使用此信息來捕獲錯誤。讓我們使用 Vector
中的 vAdd
方法來執行兩個 Vectors
之間的成對加法(pairwise addition):
val l1 = 1 :#: 2 :#: 3 :#: VNil val l2 = 1 :#: 2 :#: 3 :#: VNil val l3 = l1 vAdd l2 // Result: l3 = 2 :#: 4 :#: 6 :#: VNil
上面的例子正常運行,因為類型系統知道兩個 Vectors
的長度都為 3。然而,如果我們嘗試 vAdd
兩個長度不同的 Vectors
,我們會在編譯時得到一個錯誤,而不必等到運行時。
val l1 = 1 :#: 2 :#: 3 :#: VNil val l2 = 1 :#: 2 :#: VNil val l3 = l1 vAdd l2 // Result: a *compile* error because you can't pairwise add vectors // of different lengths!
Shapeless 是一個了不起的庫,但就我所看到的,它仍然有點粗糙,只支持依賴類型的一個子集,並導致生成相當詳細的代碼和類型簽名。另一方面,Idris 使類型成為編程語言的首要成員,因此它的依賴類型系統看起來更強大和更干凈。為了比較,請查看演講 — Scala vs Idris: Dependent Types, Now and in the Future 。
形式化驗證方法已經存在很長一段時間了,但大多數情況下都十分麻煩,無法用於通用編程。依賴類型在像 Idris 這樣的語言中,甚至在未來的 Scala 中,可能會提供更輕量的和更實用的替代方案,這仍然能大大提高類型系統提供捕獲錯誤的能力。當然,沒有類型系統可以捕獲所有的錯誤,由於終止(halting)問題的固有局限性,但如果做得好,依賴類型可能是靜態類型系統下一個大的飛躍。
Concatenative 語言
有沒有想過在沒有變量和函數應用的情況下編程是一種怎樣的體驗?沒有?我也沒試過。但顯然有些人做了,他們提出了 concatenative 編程這個概念。這個概念背后的思想是語言中的所有內容都是一個函數,用於將數據推送到堆棧或從堆棧彈出數據;程序幾乎完全通過功能組合來構建(concatenation is composition)。簡單的說即是基於堆棧的編程語言。
這聽起來很抽象,所以讓我們來看一個簡單的例子:
2 3 +
在這里,我們將兩個數字推到堆棧上,然后調用 +
函數,它將兩個數字從堆棧中彈出,並將結果添加到堆棧中:代碼的輸出是 5。這里有一個更有趣的例子:
def foo { 10 < [ 0 ] [ 42 ] if } 20 foo
我們來一行一行看這段代碼:
- 首先,我們聲明一個函數
foo
。請注意,cat 中的函數不指定輸入參數:所有參數從堆棧中隱式讀取。 foo
調用<
函數,它會彈出堆棧中的第一個選項,將其與 10 進行比較,並將True
或False
返回到堆棧。- 接下來,我們將值 0 和 42 推到堆棧上:我們將它們放在括號中,以確保它們被壓入未被評估的堆棧。這是因為它們將被用作“then”和“else”分支(分別)用於調用下一行的
if
函數。 if
函數從堆棧中彈出 3 個選項:布爾條件,“then”分支和“else”分支。根據布爾條件的值,它會將“then”或“else”分支的結果推回堆棧。- 最后,我們將 20 推到堆棧並調用
foo
函數。 - 當所有都完成了,我們最終會得到數字 42。
有關這種語言更詳細的介紹,請參閱 The Joy of Concatenative Languages。
這種編程風格有一些有趣的屬性:
- 程序可通過無數的方式來分割和連接,以創建新的程序;
- 極簡的語法(甚至比 LISP 還小)產生了非常簡潔的程序;
- 強大的元編程支持
我發現 concatenative 編程是一個非常開眼界的體驗,但我還沒實踐過。似乎你必須記住或想象堆棧的當前狀態,而不是能夠從代碼中的變量名讀取它,這可能使得很難理解代碼。
聲明式編程(Declarative programming)
聲明式編程已經存在了很多年,但大多數程序員仍不知道它是一個怎樣的概念。簡要來說:在大多數主流語言中,你是在描述如何解決特定的問題;在聲明式語言中,你只需描述所需的結果,語言本身可推導出結果。
例如,如果你在 C 語言中從頭開始編寫排序算法,你會編寫合並排序的說明,逐步描述如何遞歸地將數據集分為兩部分並按順序將其合並到一起:這里是一個例子。如果使用聲明式語言如 Prolog 對數字進行排序,可以直接描述你需要的輸出:“我想要相同的值列表,但索引 i
中的每個項目應小於或等於索引 i + 1
中的項目”。將上面 C 語言的解決方案和 Prolog 代碼進行比較:
sort_list(Input, Output) :- permutation(Input, Output), check_order(Output). check_order([]). check_order([Head]). check_order([First, Second | Tail]) :- First =< Second, check_order([Second | Tail]).
如果你使用過 SQL,那么你已經使用了聲明式編程,可能自己沒有意識到這一點:當你發出一個像 select X from Y where Z
這樣的查詢,你就是在描述你想要返回的數據集;數據庫引擎的工作實際上是如何執行查詢。你可以在大多數數據庫中使用 explain 命令來查看執行計划並弄清楚在引擎下發生了什么。
聲明式語言的優點在於它允許你在更高層次的抽象下工作:你的任務就是描述所需輸出的規范。例如,在 Prolog 語言實現的一個簡單數獨解算器中只列出了一個數獨謎題的答案的每行、列和對角線應該是怎樣的:
sudoku(Puzzle, Solution) :- Solution = Puzzle, Puzzle = [S11, S12, S13, S14, S21, S22, S23, S24, S31, S32, S33, S34, S41, S42, S43, S44], fd_domain(Solution, 1, 4), Row1 = [S11, S12, S13, S14], Row2 = [S21, S22, S23, S24], Row3 = [S31, S32, S33, S34], Row4 = [S41, S42, S43, S44], Col1 = [S11, S21, S31, S41], Col2 = [S12, S22, S32, S42], Col3 = [S13, S23, S33, S43], Col4 = [S14, S24, S34, S44], Square1 = [S11, S12, S21, S22], Square2 = [S13, S14, S23, S24], Square3 = [S31, S32, S41, S42], Square4 = [S33, S34, S43, S44], valid([Row1, Row2, Row3, Row4, Col1, Col2, Col3, Col4, Square1, Square2, Square3, Square4]). valid([]). valid([Head | Tail]) :- fd_all_different(Head), valid(Tail).
下面是如何運行上面的數獨解算器:
| ?- sudoku([_, _, 2, 3, _, _, _, _, _, _, _, _, 3, 4, _, _], Solution). S = [4,1,2,3,2,3,4,1,1,2,3,4,3,4,1,2]
不幸的是,聲明式編程語言的缺點是性能開銷大。上面提到的排序算法時間復雜度可能是 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 Principle, Media for Thinking the Unthinkable, 和 Learnable Programming。
更新:“符號式編程”可能不太適合用於描述 Aurora。有關更多信息,請參閱 Symbolic programming 的維基主頁。
基於知識的編程(Knowledge-based programming)
很像上面提到的 Aurora 語言,Wolfram 語言也是基於符號的編程。然而,符號層僅僅是為 Wolfram 語言的核心提供一致的接口,Wolfram 語言是基於知識的編程語言:內置了大量的庫、算法和數據。這使得可以輕松地從圖形化的 Facebook 連接,到操縱圖像、查找天氣、處理自然語言查詢、繪制地圖上的方向、求解數學方式等方面做好一切。
我猜想 Wolfram 語言有最大的“標准庫”和任何現有語言的數據集。對於 Internet connectivity 是編寫代碼的固有功能,我也感到十分興奮:它幾乎像一個 IDE,其中的自動完成功能進行谷歌搜索。看符號式編程模型是否像 Wolfram 所說的那樣靈活並真正利用所有這些數據,這將是非常有趣的。
更新:盡管 Wolfram 聲稱 Wolfram 語言支持“符號式編程”和“知識編程”,但這些術語的定義有所不同,有關更詳細的信息請參閱 Knowledge level 和 Symbolic Programming 的維基主頁。
原文鏈接:https://my.oschina.net/editorial-story/blog/890965
責任編輯:開源中國 – 局長
轉載必須在正文中標注並保留原文鏈接和作者等信息