跳至內容
GitHub 儲存庫 論壇 RSS 新聞來源

Crystal 的未來

Ary Borenzweig

(這篇文章是 Crystal Advent Calendar 2015 的一部分)

在聖誕節前夕,發生了一件奇特的事情:當我們在 Crystal 中快樂地編碼時,當我們把目光從螢幕上移開的那一刻,一個半透明的身影出現在附近。這個實體走近並說道:「我是聖誕節過去之靈。跟我來。」

我們看到自己正在編寫一種新的語言,它會類似於 Ruby,但會被編譯且具有型別安全性。在那時,該語言確實很像 Ruby:要建立一個空的陣列,你會寫 [],或寫 Set.new 來建立一個空的集合。我們很高興,直到我們意識到編譯時間非常長、呈指數級增長、令人難以忍受,然後我們就感到沮喪。

我們花費了可怕的時間試圖讓它運作,但徒勞無功。最後,我們決定做出改變:指定空的泛型型別的型別,例如 [] of Int32Set(Int32).new。編譯時間恢復正常。我們再次感到有點高興,但同時也感覺我們正在拋下 Ruby 的某些感覺。該語言分歧了。

我們回頭看著聖誕節過去之靈,想問他這一切是什麼意思,但我們發現一個類似但不同的身影取代了他。她說:「我是聖誕節現在之靈。加入我。」

在我們周圍,一個小而充滿活力的社群正在使用 Crystal 編碼。他們很高興。沒有人提到必須為泛型型別指定型別的煩惱。每個人都感覺到 Ruby 的精神以某種方式仍然存在:在熟悉的 API 和類別、在語法、在強大的區塊中。此外,CPU 和並發方面的效能提升,加上更好的型別安全性,確實得到了回報,所以偶爾必須指定型別並不會感到麻煩。

我們再次看著精靈,想詢問其含義:似乎我們過去做了一個正確的決定,對吧?但是,就像之前一樣,有一個機械的結晶身影取代了他。它說:「我是聖誕節未來之靈。跟著我。」

這個小社群仍然在使用 Crystal 編碼,儘管大多數人似乎不像以前那樣高興了。我們試圖問他們為什麼,但沒有人注意到我們的存在。我們試圖使用電腦在網路上搜尋 Crystal,但我們的手無法觸碰任何東西。我們帶著好奇的表情轉向精靈,並注意到它的胸部有一個鍵盤和一個小螢幕。我們搜尋了「Crystal sucks」,希望這會顯示有關該語言的抱怨文章。事實上,確實有不少這樣的文章。大多數都是關於編譯時間長和記憶體使用量大的問題。「編譯時間長?」,我們心想。「我們在幾年前就解決了這個問題!」,我們對著精靈大喊。我們從它那裡得到的唯一回覆是「正在編譯...」,景象消失了,我們又回到了辦公室,獨自一人。

回到現在

「我們來做一些數學計算」,我們說。我們現在在 Crystal 中最大的程式是編譯器,它大約有 4 萬行程式碼。編譯它大約需要 10 秒鐘,並且需要 940MB 的記憶體。我們的一個 Rails 應用程式,計算其 gem 和「app」目錄中的程式碼總行數,大約有 32 萬行程式碼,比編譯器大 8 倍。如果我們用 Crystal 重寫它,或者至少開發一個具有類似功能的應用程式,則每次編譯它都需要 80 秒,並且需要 8GB 的記憶體。每次變更後都需要等待很長時間,而且記憶體使用量也非常多。

我們是否可以在目前的語言中改善這種情況?我們是否可以引入增量編譯?我們花了一些時間思考如何快取先前的編譯的語義結果(推斷的型別),並將其用於下一次編譯。一個觀察結果是,方法的型別完全取決於參數的型別、它調用的方法的型別,以及實例、類別和全域變數的型別。

因此,一個想法是快取程式中所有型別的推斷實例變數型別,以及方法實例化的型別及其依賴關係(該方法依賴哪些型別,以及具體調用哪些其他方法)。如果實例變數型別保持不變,方法的程式碼沒有變更,並且依賴關係(調用的方法)沒有變更,我們可以安全地重複使用先前編譯的結果(型別和產生的程式碼)。

請注意,上面的「如果」以「如果實例變數型別保持不變」開頭。但是我們如何知道這一點?問題在於,編譯器通過遍歷程式、實例化方法並檢查分配給它們的值來確定它們的型別。因此,我們無法真正重複使用快取,因為在對整個程式進行型別檢查之前,我們無法知道最終的型別!這是一個雞生蛋的問題。

解決方案似乎是必須指定實例、類別和全域變數的型別。這樣一來,一旦我們對方法進行型別檢查,其型別就永遠不會改變(因為對於方法而言非本地的一切,例如實例變數,都不能再更改)。我們將能夠快取該資訊並在下一次編譯中重複使用它。不僅如此,即使沒有快取,型別推斷也會變得更簡單、更快。

這樣做是對的嗎?我們將再次稍微偏離 Ruby。我們想要什麼樣的未來?我們是否要堅持目前的方法,代價是每次編譯之間都必須等待很長時間?還是最好指定更多型別,但擁有更敏捷的開發週期?

我們真正想要的是一種使用起來有趣且高效的語言。等待編譯完成是很無聊的,即使偶爾需要註解一些型別,也比等待編譯完成好得多。這些型別僅用於泛型、實例、類別和全域變數:局部變數和方法參數不需要型別註解。考慮到這些型別的變化頻率相較於您編寫新方法和編譯程式的次數是如此之少,因此這似乎是值得改變的事情。

我們已經開始開發這個新的編譯器,因為我們希望盡快完成它,因為那樣會破壞很多現有的程式碼。當目前的編譯器直接在 AST 上運作時,在新的編譯器中,我們使用 流程圖,這將使我們擁有一個更簡單的編譯器(任何人都可以理解並立即投入其中並做出貢獻),並且更容易理解和最佳化程式碼。它還可以讓您以最少的精力引入新功能,例如 Ruby 的 retry,因為流程圖允許循環和類似「goto」的跳轉。

如果您想了解更多關於此變更的信息,有一個關於此變更的 追蹤問題

問與答

  • 您何時完成新的編譯器? 我們還不知道。我們正在緩慢但穩步地開發它,撰寫時考慮到可讀性、可擴展性和效率,並首先關注最困難的部分。現在我們知道該語言支援的大部分功能,這就變得更容易了。請記住,目前的編譯器最初是一個實驗,也是一個用 Ruby 編寫的編譯器的移植版本,所以它的程式碼並不是最好的 Crystal 程式碼。
  • 您是否會繼續開發目前的編譯器? 是的,也不是。如果它們很容易修復,我們會修復錯誤,並且我們會繼續擴充和改進標準函式庫。
  • 我的所有程式碼都會停止編譯嗎? 很有可能。但是,您可以使用目前編譯器的 tool hierarchy 來查詢實例變數的型別,以使升級更容易。事實上,我們可能會包含一個工具來自動執行升級,它真的非常簡單。
  • 新的編譯器是否會包含其他功能? 我們希望如此!通過此變更,我們還計劃使用常用的 &block 語法來支援轉發區塊。現在這是有可能的,但它總是最終建立一個閉包,但是這可以做得更好。我們還計劃允許使用區塊進行遞迴呼叫,這在 Ruby 中可以做到,但在 Crystal 中則不行。我們還希望能夠擁有任何種類的 TArray(Object)Array(T),這同樣是目前語言版本不太可能實現的。因此,這些新的型別註解將為該語言帶來更大的效能作為補償。
  • 未來是否會有更多像這樣的重大變更? 我們幾乎可以肯定答案是否定的。如果我們知道實例、類別和全域變數的類型,那麼給定一個方法,self 的類型及其參數的類型,我們就可以僅透過分析該方法及其調用的方法來推斷其類型。目前這是不可能的,因為某些方法的類型取決於您如何使用類別(您將其賦予什麼)。因此,這次的變更將是最後一次重大變更。