奧推網

選單
科技

“我是如何從 Python 換到 Common Lisp,又換到 Julia 的?”

摘要:作為一名程式設計師,很少有人在從業生涯中只接觸過一種程式語言,本文的作者就是如此。最初,身為圖形程式設計師的他喜歡 Python 的快速易用,後來他發現 Common Lisp 更加高效,但用了 7 年後,他最終換成了與 Common Lisp 非常相似但更有優勢的 Julia。

原文連結:https://mfiano。net/posts/2022-09-04-from-common-lisp-to-julia/

作者 | Michael Fiano譯者 | 彎月

出品 | CSDN(ID:CSDNnews)

我想透過本文講述我選擇的主要程式語言從Common Lisp過渡到Julia的經過及原因。本文僅代表我個人的經驗和看法。

這兩種語言的設計都非常優秀,而且執行良好,所以我鼓勵你親身研究一下,看看哪種程式語言最適合你。

從 Python 到 Common Lisp

我第一次接觸Common Lisp是在 2008 年 1 月。在此之前,大部分時候我都在使用Python,有一位同事偶爾會笑話我的程式碼,然後用Common Lisp重新實現部分程式碼,希望能改變我對Common Lisp的看法,然而他的努力都白費了。我清楚地告訴自己,永遠也不會使用帶有如此多括號和結構的語言,這種語言太不正常了。我使用Python就是因為這種語言可以輕鬆快速地將我的想法轉化為程式碼。

有一天,純粹因為太無聊,我隨手翻開了《Practical Common Lisp》,這是一本經常被人推薦的Common Lisp入門參考書。幾天之後,雖然我還沒有讀完這本書,但我發現自己已經愛上了這些括號。我是如何從一個極端走向另一個極端的?答案很簡單:雖然我只是一個Lisp初學者,但我用Common Lisp編寫程式碼的效率竟然比Python還高,儘管我擁有多年的Python開發經驗。在我看來,這太不可思議了,因為Python經常自詡非常適合快速開發應用程式。

從2008年~2022年,我每天都在使用Common Lisp編寫程式碼,而且只用這一種語言。我能夠快速有效地將自己的想法轉化成程式碼。這都要歸功於Common Lisp是圍繞互動性構建的。我可以一邊執行Lisp映象,一邊修改程式碼,不斷完善程式,也可以做任何我想做的事情,而且每次修改程式碼後可以立即得到反饋。鑑於我是一名圖形程式設計師,這種互動然後迭代的開發工作流程對我來說尤其重要。我的工作是編寫遊戲引擎、遊戲、圖形演算法、過程生成影象、模擬以及其他相關的應用程式。

從 Common Lisp 到 Julia

在使用Common Lisp做了7年的軟體開發後,2015年,我開始對這門語言的各個方面都感到越來越失望,所以我開始尋找第二種語言,畢竟我們都知道不要把所有雞蛋都放到一個籃子裡。就在這個時期,我聽說了Julia。於是,我通讀了整本手冊,這門語言留給我的印象很深刻。然而,當時Julia還處於起步階段,所以我也沒有做進一步的探索。

於是,我繼續使用 Common Lisp,直到2017年,一位有名的Lisp程式設計師宣佈改用Julia。雖然我不是資料科學家,但他的文章(https://tamaspapp。eu/post/common-lisp-to-julia/)重燃了我對Julia的興趣,這次我決定親身嘗試一下。至於後面的故事嘛,長話短說,我經歷了一段非常艱難的時期,由於當時Julia還不穩定(距離1。0版本釋出還有一年的時間),有很多出乎意料的事情。所以,我只好暫時放棄,只在REPL實驗中偶爾使用Julia,但我一直在留意後續版本的發展。

2020年,我的一個長期專案遭遇滑鐵盧,以此次事件為契機,我決定向其他領域過渡,但仍然是與圖形相關的工作。就這樣,我開始使用Julia編寫了一個簡單的圖形數學庫,但沒過多久我就開始懷念 Common Lisp。然而,這是我第一次嘗試編寫 Julia 程式碼,而且這門語言給我留下了深刻的印象。最終,我還是回到了 Common Lisp,編寫了各種小型實用程式和圖形演算法。

時至2022年,我意識到,如果因為其他語言缺乏Common Lisp的一些特性而將它們拒之門外,恐怕自己就要永遠“困”在Common Lisp中了。我不敢奢望某種語言能夠聚集所有這些特性。於是,我開始學習一些程式語言,包括但不限於 Scheme、Racket、Raku、Go 和 Rust。在這當中,Rust是唯一一個我之前就研究了很多年的語言,只不過在當時它還沒有這麼流行。我決定重新探索Rust,看看自己能否接受它。然而,最終的答案是否定的,而且我嘗試過的所有其他語言也都是否定的。

2022 年 6 月,我強迫自己停止使用 Common Lisp,並利用閒暇時間,花了幾周使用Julia編寫了一個小型數學庫。在編寫這個庫的過程中,我看到了 Common Lisp 和 Julia 之間的相似之處。我發現我更加欣賞Julia,於是我又編寫了一個庫,這個庫收到了社群的很多積極的反饋。一直到了9月,我都沒有再碰過Common Lisp,也沒有回頭的打算。似乎Julia可以滿足我所有的需求。

為什麼我不再使用 Common Lisp

Common Lisp 是一門很棒的語言,但它不適合我。這門語言的問題大多是社會問題,而不是技術問題。隨著時間的推移,我遇到的以下問題導致我對該語言越來越失望。

編輯器支援

Common Lisp的設計決定了它不適合在任何舊的文字編輯器或 IDE 中編輯。它的設計完全基於互動性和迭代式開發,這遠比Julia等其他語言的增量式編譯或熱過載要激進得多。CLOS 和 Common Lisp等條件系統天然具備高度的動態和互動性。

例如,當出現異常情況時(不僅僅是出錯時),編輯器中會“彈出”偵錯程式。你可以檢查任何堆疊幀中的區域性變數的狀態,檢視任何堆疊幀的上下文中的任意程式碼等等。在偵錯程式處於活動狀態期間,程式基本上處於“暫停”狀態,你可以自由地編輯函式和其他定義,然後告訴偵錯程式在你的修改基礎之上繼續執行程式。你甚至可以透過程式設計的方式控制該行為,而且還可以控制是否展開堆疊,並自定義異常狀況的處理。大多數其他語言中的“異常系統”只相當於Common Lisp條件系統的一小部分,而且關鍵是,在處理異常之前,呼叫堆疊就展開了,所以大多數情況下你沒辦法正確地處理異常。

你可以透過Common Lisp 檢查器直觀地遍歷物件層次結構,探索有關程式狀態的每一個細節,甚至修改它們的值,所有這些都是編輯器內建的功能。

編譯函式和其他定義也非常簡單,只需要將游標懸停在編輯器中相應的定義上,然後按下一個快捷鍵。而且編譯結果是實時的,執行中的程式會立即看到變更。

所有這些互動性都是透過一個編輯器外掛實現的,它遠遠超出了語言伺服器協議伺服器的範圍。因此,任何非 Emacs 的編輯器對它的支援都非常有限。雖然其他編輯器也有支援,但遠不如 Emacs方便和完整。

我不喜歡被迫使用特定的編輯環境來用一種語言程式設計。我是一個 Vim 使用者,雖然Vim也有許多類似的外掛,但使用起來非常彆扭,並且提供的功能也只有Emacs外掛的一部分。雖然我們可以透過設定,讓Emacs表現出與Vim類似的行為,但 Emacs 的複雜性和蹩腳的功能依然會時不時冒出來。雖然多年來我一直被迫使用Emacs,但我一點都不喜歡這種編輯器。

語言發展

Common Lisp 不只是一門語言,它還是描述如何實現語言的文件。因此,這個ANSI標準有多種實現方式,每種實現方式都有自己的一組特性。

有時編寫可移植的 Common Lisp 難度非常大,甚至根本不可能。我們經常需要使用可移植性庫來統一特定功能在不同實現中的介面。

Common Lisp是一個標準,這一事實既是福也是禍。許多開發人員認為這是好事,因為編寫的程式碼無論過多久都不會出問題。但對於某些人來說,這意味著該語言不會再發展了。例如,

Common Lisp的誕生早於Unicode;

沒有執行緒的概念;

不強制要求 IEEE-754 浮點編碼;

沒有描述外部功能介面(FFI);

沒有定義垃圾收集介面;

沒有網路支援;

等等。所有這些特性都需要第三方庫來實現,甚至還需要可移植性庫來統一不同實現之間的介面。

此外,這個標準非常難以學習,尤其是對於嘗試學習該語言的初學者。標準的許多地方都很模糊,甚至是錯誤的,並且經常在 Common Lisp 交流論壇中引發長時間的爭論。

軟體的版本與部署

Common Lisp沒有內建的包管理器。安裝新軟體需要透過第三方庫進行,通常包含在每一種Common Lisp的實現中,但並不是語言標準的一部分。許多實現經常會包含非常古老的包管理器,所以經常需要自行替換掉。而且,這個軟體(ASDF,https://asdf。common-lisp。dev/asdf。html)不支援軟體下載,只能安裝本地已經下載好的軟體。

還有另一個第三方軟體Quicklisp,它是ASDF的一個封裝,支援從網際網路上下載新版本軟體,然後轉給底層的ASDF工具進行安裝。

雖然Quicklisp能很好地處理小型的開源Common Lisp社群版,但遠不理想。它本身包含了一個發行版——“quicklisp”發行版。這是一堆用來描述軟體包的元資料,描述了每個軟體包的最新版本是什麼、可以從哪裡下載,以及依賴項是什麼。軟體需要透過官方Quicklisp的發行版庫安裝,而不是從上游庫進行安裝。

Quicklisp由其維護者負責管理,他們負責保證所有軟體都能正確構建(只是軟體本身能構建而已,不會檢查軟體是否與同一發行版中的其他軟體相容)。Quicklisp的維護者每隔一兩個月從上游下載軟體包,並放到一個發行版中。這個過程不會考慮相容性資訊,使用者只能收到最新版的軟體,而這個版本並不一定能與其他軟體相容。

實際上,Common Lisp的開發者們甚至都不會給軟體新增版本號,因為他們把版本管理的責任完全推給了Quicklisp的維護者,而自己什麼都不管。在我看來,這個模型遠遠沒有達到正規軟體開發的標準。對於使用者會收到哪個版本的軟體,開發者完全不知情,除非開發者在Quicklisp的程式碼庫上建立一個分支來跟蹤,而不是像現在這樣只更新自己的程式碼庫的主分支。但是,就算你這樣做,你的使用者在安裝“穩定版”軟體時,其依賴項也不一定會採用這種方法,因此除非開發者能自己維護一份特殊的Quicklisp發行版,否則根本沒辦法保證軟體與依賴項之間的相容性。不過,大多數使用者不會安裝內建的Quicklisp之外的發行版。Common Lisp的開發者似乎並不關心能否產生可重複的構建,也不願意擔負起軟體部署的責任。

由於Quicklisp的官方發行版每一兩個月釋出一次(對於軟體界來說,這個時間間隔太長了),開發者沒辦法即時推送補丁,也不能解決使用者報告的問題,除非自己維護並說服使用者使用自己的發行版,或者說服使用者直接從上游庫中下載原始碼,並放在硬碟的特定位置上,讓Quicklisp覆蓋發行版自帶的軟體版本。

我始終認為,軟體開發者應當負起部署和維護自己的軟體的責任,而不是依賴於Quicklisp。

文件

Common Lisp庫幾乎沒有文件。“docstring”和程式碼中的註釋並不是使用者文件。被廣泛接納的成功開源專案必須有真正的離線使用者文件,裡面有很多的使用範例、教程、圖片,以及能幫助使用者熟悉軟體的一切。這是顯而易見的,尤其是Common Lisp面向的物件主要是其他開發者,他們最喜歡閱讀程式碼並隨意修改。

軟體質量

Common Lisp的軟體通常質量很高——考慮到許多軟體都是由一個人負責開發,然後很快就被遺棄的現狀。Common Lisp的程式設計師的思維特別活躍,也非常善於寫程式碼,但通常都是單打獨鬥,所以常常無法考慮到所有邊界條件、找到重大bug、編寫文件,興趣也不會在同一件事情上停留太久。

這揭示了Common Lisp的一個非常現實的問題:它更容易吸引那些“非我所創綜合徵”(指傾向於自己創造解決方案,而非採用已有方案)且拒絕合作的人們。許多軟體的功能都大幅度重疊,而且對於新人來說,有許多唾手可得的成果誘惑著他們去創造。再加上語言本身很小,所以這種現象對語言的傷害非常大。

在我看來,這要歸罪於語言強大的可塑性。重新實現一個想法,遠比使用或修改其他人的實現要容易得多。這是因為Common Lisp的程式碼非常靈活,能夠完全反映我們的思維過程。畢竟,程式碼只不過是一個人的思維過程的投影。

這個問題甚至有遞迴的效果,我們已經有了許多“半吊子”解決方案,而下一輪人們又會在其上製造出更多的半成品。

Common Lisp的靈活性養育了更多個人開發者,這個現象在庫生態系統中尤為明顯。

程式設計正規化

Common Lisp是一門多正規化程式語言。儘管如此,它最強大的功能還是面向物件正規化。雖然你可以混合使用不同的正規化,但幾乎無法避開面向物件,而面向物件正是Common Lisp深入骨髓的正規化。

雖然眾說紛紜,但CLOS(Common Lisp物件系統)是Common Lisp最好的特性之一。語言本身就建立在面向物件之上,儘管許多現代程式設計師並不認識它的這種極其特殊的面向物件。CLOS與其他物件系統的主要區別是,它將方法從類中分離出來,從而解決了一些長期困擾其他面向物件實現的問題,比如多重繼承。此外,CLOS還可以透過MOP(Meta-Object Protocol,元物件協議)進行完全定製,因此人們可以在任意層面上改變物件系統的行為。

儘管我認為CLOS非常優秀,而且必不可少,但我也相信,從通常意義上來看,面向物件並不適合所有問題。實際上,對於大多數應用,我更希望採用引數多型,而不是使用Common Lisp的泛型函式臨時創造的多型。

Common Lisp中的泛型函式不支援引數個數過載,這一點Julia和其他支援泛型函式的語言都支援。在Common Lisp中泛型函式的引數個數是固定的,並且規定了一種協議。許多人喜歡這一點,但我不贊成。

此外,Common Lisp的大部分特性都不支援泛型。例如,你無法重新定義序列的特性,無法將序列擴充套件成其他集合型別或迭代器。一些實現支援一種名為“可擴充套件序列”的擴充套件,部分解決了這個問題,但由於只有少數幾個實現支援這一特性,因此並不具備可移植性。原因是,在Common Lisp中呼叫泛型程式碼會導致效能略微下降,因此在注重效能的應用中,人們不得不使用非泛型的程式碼。

Common Lisp中的使用者自定義型別有三種形式:類,結構體,型別。defclass定義類,defstruct定義結構體,deftype定義型別。

結構體很少用到,除非在注重效能的程式碼中,因為它的工作方式無法與Common Lisp的互動式開發流程完美結合,重新定義結構體會導致未定義的行為。此外,它們只支援單繼承。

使用者自定義型別只是已有型別的別名。

泛型函式對引數進行特化只能在類名上進行,無法在型別上進行。儘管每個類都有同名的型別,但反過來並不成立。例如,你無法定義一個方法,對某個範圍 [0, 10) 進行操作。但是,可以針對一個值進行操作,前提是這個值可以透過相等操作進行比較,也就是說,只能用於標量上,而不能用於集合值上。

儘管CLOS非常強大,而且可以實現各種技巧,但我幾乎每天都會遇到這些涉及根本的問題,我真心希望它能支援帶有泛型型別引數的引數化多型。

社群

開源Common Lisp社群是我見過的最小的程式語言社群。獲得幫助很困難,找人合作專案更困難,因為大多數開發者都是單獨作戰。

獲得幫助也很困難。有經驗的Common Lisp開發者會假設你對語言有基本的瞭解,知道風格上的最佳實踐,並且熟悉Emacs,安裝了相關的Common Lisp工具。作為初學者,尋求幫助並貼一段程式碼,得到的回答往往是要求先修正程式碼風格問題才能提供幫助。這非常打擊初學者,讓他們喪失學習的信心。在極端情況下(而且並不罕見),社群對於新手問題的容忍度非常低,因為人們經常會因為術語錯誤或不恰當的程式碼格式而怒不可遏。

即使作為有經驗的Common Lisp開發者,我也不喜歡參與討論,因為這有損健康。社群應該有更友好的氛圍。

效能

如果你的目標是極致的效能,那麼Common Lisp絕不是最佳選擇。儘管你可以編寫出速度可與C語言媲美的程式碼,但這很大取決於Common Lisp的實現,以及與互動性的取捨。

要想獲得執行時效能提升,可以不使用泛型函式的動態分發特性,這就意味著不能使用CLOS的類程式設計。相反,必須使用結構體來建立集合型別,因為其欄位訪問器不是泛型函式,而是正常函式,而且重新定義會導致未定義的行為。

型別註釋通常要分散到整個程式碼中,還要使用宏和編譯器宏,在編譯時將程式碼變成效率更高的形式。

任何立即值都要特化並放到記憶體中,而不能採用指向堆記憶體的指標。可以特化的元素型別完全依賴於具體實現,而且幾乎只能使用標量。結構體陣列、陣列的陣列以及許多其他資料型別基本上無法在不影響效能的前提下使用。

並不是每個人都能寫出高效能的Common Lisp程式碼,而且這幾乎是一件不可能完成的任務。此外,有些程式碼在一種實現上執行的速度很快,但換到另一種實現上可能就會變得非常緩慢。在我看來,只要開始編寫依賴於具體實現的程式碼,就意味著該換一種程式語言了。

為什麼我選擇了Julia作為主程式語言

Julia是一種非常強大的語言。至於我個人喜歡這門語言的原因,我總結了一下,大致如下。簡單來說,Julia與Common Lisp非常相似,此外Julia還有許多優勢。

編輯器支援

我認為Julia的編輯器支援是最棒的。你不需要使用特定的編輯器就能獲得Julia的完整體驗。各種編輯器都有十分優秀的外掛,甚至有專門的組織負責改進Julia的編輯器工具。

我個人喜歡使用Vim和LaugnageServer。jl,體驗非常好。

語言發展

Julia與Common Lisp不同,它沒有語言標準,可以自由發展,因此可以解決更多的問題。

我認為這是一件好事。我不希望一門語言故步自封。我希望看到進步,而在過去幾年,Julia取得了很多進步,採用了許多新特性,新軟體包的出現速度也越來越快。

軟體版本和部署

與Common Lisp不同,Julia的開發者可以全權負責軟體的釋出。不過,有了內建的包管理器Pkg。jl,以及軟體包倉庫和相關工具,這一切都非常容易。

我將我的軟體託管在GitHub上,而將軟體釋出給Julia社群的過程也非常容易。我只需要在希望釋出的提交上新增一個註釋,一個GitHub機器人就會負責分析軟體,如果符合要求,就將其合併到官方軟體倉庫中。使用其他託管平臺甚至自行託管時的流程也非常相似。

在軟體包進入官方倉庫後,任何人都可以使用內建的包管理器進行安裝。

構建的可重複性也非常好。所有Julia專案環境都帶有一個清單檔案,記錄了所有依賴項的精確版本,如果專案名稱在多個倉庫中出現,那麼還會記錄一個唯一的識別符號,以解決歧義問題。只需把這個清單發給其他人,他們就能復現出完全相同的開發環境。非常容易。

Pkg。jl是一個設計得非常好的包管理器,提供許多有用的功能。為強烈建議你閱讀一下文件,瞭解一下它的基本功能。它是迄今為止我用過的最好的包管理器。

程式設計正規化

Julia不是一門面向物件語言。實際上,它根本沒有類。它有一個特別的型別系統,初看之下似乎功能非常有限,只能例項化型別的樹形結構的葉節點。中間節點是抽象型別,不能被例項化,但可以用來定義所有類的行為,甚至可以用trait對類進行解構。

只能例項化葉節點這一點看起來似乎有點受限,但實際上大幅度簡化了泛型函式的可應用性,因此閱讀程式變得非常簡單,而且可以鼓勵程式設計師採用組合結構來代替繼承結構。

型別引數化和零成本泛型函式是我切換到Julia的主要原因之一。編寫高效率的程式碼非常容易,不需要犧牲程式碼的清晰度。實際上,除了需要很好地理解資料結構和演算法(這是每個程式設計師的基本功)之外,我根本不需要考慮任何效能有關的問題。

關於Common Lisp與Julia的泛型函式,另一個需要指出的區別是,Julia的泛型函式中的可選引數可以進行特化,這樣就可以針對不同數量的引數生成多個方法。而Common Lisp禁止簽名不同的泛型函式,所以Common Lisp中無法實現這一點,除非採用元物件協議擴充套件。

社群

Julia社群是最好的社群之一,而且非常大。它分成多個子社群,比如Slack、Zulip、Discourse、Discord和IRC等。我是Julia Zulip社群的活躍使用者之一,在那裡我提出的問題幾乎能得到及時的答覆。此外,這些人還非常歡迎新使用者,讓我感覺自己也是他們的一員。

Julia社群是高質量社群最好的榜樣。

效能

Julia程式碼的執行速度非常快。即使不理解常見的陷阱(如抽象型別欄位、型別不穩定性)等,程式碼的執行速度也遠超Common Lisp,儘管後者經過了大量手工最佳化、且採用了能生成高效機器碼的實現。

實際上,我的第一個專案從Common Lisp移植到Julia,效能就得到了數量級的提升,而我完全沒有采用任何最佳化措施。這對我非常有吸引力,因為我在Common Lisp上花費的大量時間都用來最佳化程式碼了。

Julia的效能非常好,而且可讀性非常高。

在最新的一個移植專案中,我花了一些心思來最佳化最終穩定版,結果效能提高了大約兩到三個數量級。以前執行時間為毫秒級的函式,在Julia中降到了納秒級別,而且並沒有影響到程式碼質量。

這要歸功於Julia強大的JIT編譯器,以及豐富的型別系統。Julia中的泛型函式沒有任何效能開銷,甚至還支援型別引數化,因此我們可以針對一大類的型別定義功能,而不會影響到執行時的效能。

總結

我關注Julia已經很多年了,如今它已成為我的日常程式語言。

Julia在型別和泛型程式設計方面投入了很大精力,因此它可以非常優雅地解決複雜的問題,而不需要使用任何型別註釋。Julia的型別推斷引擎非常優秀,經常會出乎意料地向我證明它的強大之處。

另一個理由是,你可以透過任何編輯器編輯Julia。我非常不喜歡Emacs。

此外,Julia的社群非常友好,知識積累豐富,也非常有包容心。

總的來說,在熟悉Julia之後,你就能感覺出Julia的設計者非常精通Common Lisp,他們在嘗試透過更自然的方式來表述Common Lisp的思想。我認為Julia也是一種Lisp語言,只不過沒有大量括號。仔細觀察就會發現,Julia和Common Lisp真的很相似,因此我的過渡非常輕鬆。