0%

【文章翻譯】Evolving the Dart REPL PoC

【文章內容使用 Gemini 1.5 Pro 自動翻譯產生】

演進 Dart REPL 概念驗證

Dart REPL 允許您在 互動式 shell 中評估 Dart 表達式和語句。自從我 第一次發佈關於 Dart REPL 的文章 以來已經有一段時間了(您不需要閱讀它就能享受這篇文章),而且仍然缺少許多功能。特別是,動態導入和對頂級宣告的支援將非常有用,所以讓我們來看看如何支援它們。

免責聲明:我的確在 Google 工作,但這篇文章是關於一個個人專案。我不在 Dart 團隊或相關團隊。本文僅包含我個人的淺見。

tl;dr:關於如何運行 Dart REPL 的程式碼和說明可以在 https://github.com/BlackHC/dart_repl 找到。

熱重載

對於 Flutter,Dart VM 中加入了一項很酷的新功能:熱重載。Dart DevSummit 上有一個有趣的 YouTube 影片,詳細解釋並展示了它:

熱重載允許您在程式運行時更改程式碼。Dart VM 將擷取您所做的更改,並嘗試應用它們,同時保持一切正常運行。如果不能,它會告訴您原因。這非常酷!本著駭入 Dart 以做偉大的事情的精神,讓我們思考如何使用它來實作新功能。

為什麼我們不能在目前版本的 REPL 中導入新的函式庫?

REPL 使用 Dart 的 VM 服務來評估表達式。遺憾的是,導入函式庫在 Dart 中不是表達式,因此我們不能僅在該上下文中評估它。但是,我們可以在 REPL 的沙盒運行時更改其程式碼以導入新的函式庫,然後我們可以觸發熱重載來更新 REPL。這樣可行嗎?實際上可行 \o/

具有運行時導入的 Dart REPL

但是等等:我們透過 VM 服務評估表達式無法做的另一件事是建立新的類別和函數。事實上,由於這個原因,Dart 的任何頂級宣告都不能透過評估表達式來執行。

我們如何允許頂級宣告?

當然,我們可以使用上面描述的相同想法來加入新的類別或全域函數。但是,任何使用過 IPython 或類似工具一段時間的人都知道,您往往會在迭代程式碼時經常重新宣告相同的類別或函數。在您試用程式碼時,您會一遍又一遍地重新執行略微修改過的相同程式碼版本。

如果我們只是將這些宣告加入到我們的沙盒 Dart 函式庫中,則需要我們記錄在檔案中何時何地宣告了什麼,以便在您迭代它時更新宣告。這需要大量的邏輯和聰明的程式碼。遺憾的是,如果對類別的更改與舊程式碼或其他宣告不相容,它也容易損壞。這將阻止 REPL 熱重載,並迫使用戶重新啟動它 :( 這聽起來很複雜且脆弱:我認為這不是一個成功的組合!

Spike 和鏈

相反,如果我們可以多次重新定義相同的頂級宣告而不會發生重新宣告衝突怎麼辦?這在 Dart 中可能嗎?當然可以!但不是在同一個函式庫中 :) Dart 允許您導入一個函式庫,然後宣告一個遮蔽現有宣告的類別、函數或全局變數。

在此範例中,b.dart 的 MyClass 遮蔽 a.dart 的版本不會有任何抱怨,因為它們位於不同的函式庫中,並且 b.dart 中的本地宣告優先於從 a.dart 導入的宣告。

一般來說,當您宣告一個變數來隱藏來自外部作用域的另一個變數時,就會發生遮蔽。例如:

我們可以使用這個嗎?為了研究它,我實作了一個快速 spike 在這裡。它不會產生任何程式碼。相反,這是一個非常愚蠢的範例,用於確保我們認為可行的方法實際上可行。如果花費大量時間使用程式碼生成來實作它,卻發現它永遠不可能工作,那將會令人沮喪!這是它的要點:

這確實可行!我們可以建立一個相互導入的函式庫鏈(並且也相互匯出,因為否則符號將不會在任何地方都可用)。然後,用戶可以根據需要重新定義符號。顯然,這可能導致舊程式碼引用被遮蔽的符號,這可能會使事情稍微混亂,但至少它不會損壞。任何使用過 IPython 或類似工具的人也都學會了忍受它。它不可能那麼糟糕。

上圖顯示了它是如何工作的:當我們加入新的頂級宣告時,會建立新的「單元格」(Dart 函式庫),這些單元格導入(並匯出)前一個單元格。最終單元格被導入到沙盒函式庫中,該函式庫用作普通 Dart 表達式和語句的執行環境。沙盒檔案被就地編輯,然後使用熱重載重新載入。

工作流程願景

此外,如果您想在不遮蔽任何內容的情況下連續更新程式碼,這也是可能的:熱重載已經允許在普通 Dart 程式中使用此工作流程。您可以在 REPL 中執行相同的操作。您可以編輯您的神奇 Dart 函式庫 amazing_dart_library.dart 並將其導入 REPL,試用它,並且在您這樣做的同時,您可以在您選擇的編輯器中編輯程式碼,並讓 REPL 在您想要時透過調用 reload() 熱重載程式碼。兩全其美 \o/

我們如何在實踐中實作它?

好吧,我們在這裡駭入 Dart,所以讓我們看看:熱重載尚未得到 vm_service_client 的支援,因為它是一個如此新的功能,並且 服務規範 尚未完全完成。我開始為 Natalie(維護者)撰寫 一個 pull request 來加入對它的支援,但實際上,正如我的同事所知:生產品質的程式碼不是我的專長,尤其不是在我的業餘時間(對不起,Natalie!)。但是,這並不會阻止我們的駭客冒險。

Pub,Dart 的套件管理系統,不僅支援自動版本約束解析和套件的集中式儲存庫,還允許您 使用本地套件或直接依賴 GitHub。通常不建議這樣做,因為您會失去許多使 pub 變得偉大的東西,但在這裡它可以工作:我只是將 vm_service_client fork 到我自己的 GitHub clone 中,並進行了必要的更改。您可以在 https://github.com/BlackHC/vm_service_client/tree/reload_sources_poc 找到程式碼。之後,我更改了 Dart REPL 的 pubspec.yaml 以連結到我的 GitHub clone,而不是官方版本:

就是這樣!在終端機中簡單的 pub get 現在會更新 Dart REPL 以使用 fork 的版本。

這使得試驗任何東西都非常容易:您可以 fork 其他套件來試用,並且輕鬆地依賴它們。而且很酷的是,我可以發佈它,當您使用 pub 為自己下載 REPL 時,它也會從 GitHub 獲取程式碼。非常容易駭入,但也可以分享!(即使通常不建議用於生產套件 :) )

主要的一點是有趣的邏輯是單元格生成器,它實作了一個非常簡單的模板機制,與我們上面討論的一致:

當需要新的導入時,會從 REPL 中調用熱重載功能:

差不多就是這樣!您可以在 pull request 中查看所有更改:https://github.com/BlackHC/dart_repl/pull/2
我承認程式碼有點駭客,而且不整潔。在 pull request 中,REPL 和沙盒之間的消息傳遞也有一些無關的包裝程式碼。遺憾的是,這有點掩蓋了主要的更改。我需要看看我們如何重構所有這些,讓它再次變得更整潔…但有時,快速讓事情運行起來比撰寫最好的程式碼和 pull request 更容易。對此感到抱歉!

帶有頂級宣告的 Dart REPL

Dart REPL 的原始碼可以在 https://github.com/BlackHC/dart_repl 找到。除了支援頂級宣告之外,我還加入了對內建 importloadPackagereload 命令的支援。(請注意:loadPackage 需要即將發佈的 Dart SDK 1.24 開發構建版本。否則它就是一個無操作。)這些內建命令都是使用熱重載的簡單擴展。最後,為了從您的本地 pub 快取中載入新的套件,我使用了優秀的 pub_cache 套件。

要試用它(並假設您已經安裝了 Dart SDK),只需運行:

1
2
pub global activate dart_repl
pub global run dart_repl

感謝您閱讀到本文的結尾!請告訴我您的想法 :)

乾杯,
Andreas


演進 Dart REPL 概念驗證 最初發佈在 dartlang 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。