Clojure

在 REPL 中進行程式設計:提升您的 REPL 工作流程

在這個階段,您已經足夠了解 REPL 的運作方式;現在我們將專注於讓您在使用 REPL 時擁有更順暢的開發體驗。有許多方面可以改善

在編輯器和 REPL 之間切換很麻煩。

大多數 Clojure 程式設計師並不會在日常開發中使用基於終端的 REPL:他們會在編輯器中使用 REPL 整合,這讓他們可以直接在編輯器緩衝區中撰寫表達式,並使用一個熱鍵在 REPL 中評估這些表達式。請參閱下方的 編輯器整合 部分,以取得更多詳細資訊。

我想在 Clojure 中進行小型實驗,但在預設的 CLI 工具中撰寫程式碼很痛苦。

如上所述,其中一個解決方案可能是使用編輯器整合。請注意,某些編輯器(例如Nightcode)專門設計為提供 Clojure 的「電池組」體驗。

但是,如果設定編輯器對你的品味來說太繁重,還有更符合人體工學的終端機 REPL 程式。

我需要偵錯從 REPL 執行的程式。

REPL 絕對可以幫助你執行此操作:請參閱下方的偵錯工具和技術區段。

我發現自己在 REPL 中重複許多手動步驟來執行我的開發環境。

考慮在你的專案中建立一個「開發」命名空間(例如 myproject.dev),在其中定義用於自動化常見開發任務的函式(例如:啟動本機網路伺服器、執行資料庫查詢、開啟/關閉電子郵件傳送等)。

當我對程式碼進行變更時,通常很難在我的執行程式中反映該變更:我必須在 REPL 中進行大量手動工作才能實現它。

根據你在撰寫程式時所做的選擇,在 REPL 中與它們互動將變得或多或少實用。請參閱下方的撰寫 REPL 友善程式區段。

我想以「筆記本」格式儲存我的 REPL 會話。

Gorilla REPL是為這個目的而製作的。

我想要比 REPL 提供的更好的資料視覺化。

您可能會從專業的 Clojure 編輯器中獲得改進的視覺化功能:請參閱以下 編輯器整合 部分。

話雖如此,請記住 REPL 是個具備完整功能的執行環境:特別是,您可以使用它來啟動特殊用途的視覺化工具(包括您自己開發的工具)。例如

  • RevealCognitect REBL 是用於瀏覽和視覺化 Clojure 資料的圖形工具,支援與 Clojure REPL 的雙向互動。

  • oz 是個 Clojure 函式庫,用於顯示數值圖表

  • datawalk 是個 Clojure 函式庫,用於互動式探索複雜的 Clojure 資料結構

  • system-viz 是個 Clojure 函式庫,用於視覺化正在執行的 Clojure 系統的組成部分

我想自訂我的 REPL。

您通常可以自訂 REPL 的讀取、評估和列印方式,但執行方法取決於您的工具鏈。例如

  • 當使用從 clojure.main 啟動的 REPL(例如使用 clj 工具時)時,您可以透過啟動「子 REPL」來自訂 REPL:請參閱 clojure.main/repl

  • 當使用 nREPL 時。[1],這可以透過撰寫自訂 中間件 來完成。

我想使用 REPL 連線到實際的生產系統。

Clojure socket 伺服器 功能可用於此目的。nREPLunrepl 等工具可用於提供更豐富的體驗。

注意:您可能不需要所有這些!

根據您的專案和個人喜好,您很可能會只使用本部分中介紹的工具和技術的一小部分。了解這些選項的存在很重要,但不要嘗試一次全部採用!

編輯器整合

所有 主要的 Clojure 編輯器 都支援一種在 REPL 中評估程式碼的方式,而無需離開目前的程式碼緩衝區,這減少了程式設計人員必須執行的內容切換數量。以下是它的外觀(此範例中使用的編輯器是 Cursive

Editor REPL integration

提示:您可以將一些表達式包覆在 (comment …​) 區塊中,以便在載入檔案時不會意外評估它們

;; you would NOT want this function to get called by accident.
(defn transfer-money!
  [from-accnt to-accnt amount]
  ...)

(comment
  (transfer-money! "accnt243251" "accnt324222" 12000)
  )

從編輯器內 REPL 整合中可以期待什麼?

以下是 REPL 整合提供的部分常見編輯器指令。所有主要的 Clojure 編輯器都支援其中大部分

  • 將游標前的程式碼傳送至 REPL:評估目前檔案命名空間中游標前的表達式。對於在目前命名空間的內容中進行實驗很有用。

  • 將頂層程式碼傳送至 REPL:評估游標目前包含的最大表達式,通常是目前檔案命名空間中的 (defn …​)(def …​) 表達式。對於在命名空間中定義或重新定義變數很有用。

  • 載入目前檔案至 REPL。對於避免手動載入函式庫很有用。

  • 將 REPL 的命名空間切換至目前檔案:對於避免輸入 (in-ns '…​) 很方便。

  • 顯示內嵌評估:顯示目前表達式的評估結果在它旁邊。

  • 以評估結果取代表達式:以評估結果取代編輯器中的目前表達式(由 REPL 列印出來)。

除錯工具和技巧

雖然傳統的除錯器可以用於 Clojure,但 REPL 本身是一個強大的除錯環境,因為它能讓你檢查和變更執行中程式的流程。在這個區段,我們將探討一些工具和技巧,以利用 REPL 進行除錯。

使用 prn 列印執行中的值

可以在程式碼中的策略性位置加入 (prn …​) 表達式,以列印中間值

(defn average
  "a buggy function for computing the average of some numbers."
  [numbers]
  (let [sum (first numbers)
        n (count numbers)]
    (prn sum) ;; HERE printing an intermediary value
    (/ sum n)))
#'user/average
user=> (average [12 14])
12 ## HERE
6

提示:你可以將 prn 與 (doto …​) 巨集結合使用,也就是 (doto MY-EXPR prn),以減少加入 prn 呼叫的侵入性

(defn average
  "a buggy function for computing the average of some numbers."
  [numbers]
  (let [sum (first numbers)
        n (count numbers)]
    (/
      (doto sum prn) ;; HERE
      n)))

更進一步:'spying' 巨集

有些 Clojure 函式庫提供了 prn 的'加強'版本,這些版本更具資訊性,因為它們也會列印關於包裝表達式的資訊。例如

  • tools.logging 記錄函式庫提供了一個 spy 巨集,用於記錄表達式的程式碼及其值

  • spyscope 函式庫讓你能夠使用非常簡潔的語法插入這些列印呼叫。

更進一步:追蹤函式庫

追蹤函式庫,例如 tools.traceSayid,可以幫助你設定較大區塊的程式碼,例如自動列印給定命名空間中的所有函式呼叫,或給定表達式中的所有中間值。

攔截並儲存值

有時你想要對中間值做更多的事,而不只是列印它們:你想要儲存它們,以便在 REPL 上對它們進行進一步的實驗。這可以透過在值出現的表達式中插入一個 (def …​) 呼叫來完成

(defn average
  [numbers]
  (let [sum (apply + numbers)
        n (count numbers)]
    (def n n) ;; FIXME remove when you're done debugging
    (/ sum n)))
user=> (average [1 2 3])
2
user=> n
3

這個「內聯定義」技術在 Michiel Borkent 的這篇部落格文章 中有更深入的說明。

重現表達式的背景

在 REPL 中進行除錯時,我們經常想要手動重現我們的程式自動執行的某些動作,也就是在函式主體內評估一些表達式。為此,我們需要重建感興趣的表達式的背景:實現此目的的一個技巧是使用與表達式使用的區域變數相同的名稱和值來定義 Vars (使用 def)。以下的「物理」範例說明了這種方法

(def G 6.67408e-11)
(def earth-radius 6.371e6)
(def earth-mass 5.972e24)

(defn earth-gravitational-force
  "Computes (an approximation of) the gravitational force between Earth and an object
  of mass `m`, at distance `r` of Earth's center."
  [m r]
  (/
    (*
      G
      m
      (if (>= r earth-radius)
        earth-mass
        (*
          earth-mass
          (Math/pow (/ r earth-radius) 3.0))))
    (* r r)))

;;;; calling our function for an object of 80kg at distance 5000km.
(earth-gravitational-force 80 5e6) ; => 616.5217226636292

;;;; recreating the context of our call
(def m 80)
(def r 5e6)
;; note: the same effect could be achieved using the 'inline-def' technique described in the previous section.

;;;; we can now directly evaluate any expression in the function body:
(* r r) ; => 2.5E13
(>= r earth-radius) ; => false
(Math/pow (/ r earth-radius) 3.0) ; => 0.48337835316173317

這個技術在 Stuart Halloway 的文章 REPL 除錯:不需要堆疊追蹤 中有更深入的說明。scope-capture 函式庫是用來自動化儲存和重新建立表達式背景的手動任務。

關於 REPL 除錯的社群資源

撰寫 REPL 友善的程式

在 REPL 中進行互動式開發,雖然能賦予程式設計師很大的權力,但也增加了新的挑戰:程式必須設計成能適應 REPL 互動,這是撰寫程式碼時要特別注意的新限制條件。[2]

深入探討這個主題超出了本指南的範圍,所以我們只會提供一些提示和資源,引導你進行自己的研究和問題解決。

對 REPL 友善的程式碼可以重新定義。透過 Var 呼叫程式碼時,較容易重新定義程式碼(例如透過 (def …​)(defn …​) 定義),因為 Var 可以重新定義,而不會影響呼叫它的程式碼。以下範例說明了這一點,它會在固定的時間間隔列印一些數字

;; Each of these 4 code examples start a loop in another thread
;; which prints numbers at a regular time interval.

;;;; 1. NOT REPL-friendly
;; We won't be able to change the way numbers are printed without restarting the REPL.
(future
  (run!
    (fn [i]
      (println i "green bottles, standing on the wall. ♫")
      (Thread/sleep 1000))
    (range)))

;;;; 2. REPL-friendly
;; We can easily change the way numbers are printed by re-defining print-number-and-wait.
;; We can even stop the loop by having print-number-and-wait throw an Exception.
(defn print-number-and-wait
  [i]
  (println i "green bottles, standing on the wall. ♫")
  (Thread/sleep 1000))

(future
  (run!
    (fn [i] (print-number-and-wait i))
    (range)))

;;;; 3. NOT REPL-friendly
;; Unlike the above example, the loop can't be altered by re-defining print-number-and-wait,
;; because the loop uses the value of print-number-and-wait, not the #'print-number-and-wait Var.
(defn print-number-and-wait
  [i]
  (println i "green bottles, standing on the wall. ♫")
  (Thread/sleep 1000))

(future
  (run!
    print-number-and-wait
    (range)))

;;;; 4. REPL-friendly
;; The following works because a Clojure Var is (conveniently) also a function,
;; which consist of looking up its value (presumably a function) and calling it.
(defn print-number-and-wait
  [i]
  (println i "green bottles, standing on the wall. ♫")
  (Thread/sleep 1000))

(future
  (run!
    #'print-number-and-wait ;; mind the #' - the expression evaluates to the #'print-number-and-wait Var, not its value.
    (range)))

小心衍生 Var。如果 Var b 是根據 Var a 的值定義的,那麼每次重新定義 a 時,你都需要重新定義 b;最好將 b 定義為使用 a 的 0 元數函數。範例

;;; NOT REPL-friendly
;; if you re-define `solar-system-planets`, you have to think of re-defining `n-planets` too.
(def solar-system-planets
  "The set of planets which orbit the Sun."
  #{"Mercury" "Venus" "Earth" "Mars" "Jupiter" "Saturn" "Uranus" "Neptune"})

(def n-planets
  "The number of planets in the solar system"
  (count solar-system-planets))


;;;; REPL-friendly
;; if you re-define `solar-system-planets`, the behaviour of `n-planets` will change accordingly.
(def solar-system-planets
  "The set of planets which orbit the Sun."
  #{"Mercury" "Venus" "Earth" "Mars" "Jupiter" "Saturn" "Uranus" "Neptune"})

(defn n-planets
  "The number of planets in the solar system"
  []
  (count solar-system-planets))

話雖如此,衍生 Var 失效的問題可能會透過以下方式得到令人滿意的緩解

  1. 確保 Var 沒有衍生到不同的檔案中,並且在變更時重新載入整個檔案;

  2. 或使用 clojure.tools.namespace 等工具程式,讓你追蹤變更的檔案並依序重新載入它們。

對 REPL 友善的程式碼可以重新載入。確保重新載入命名空間不會改變執行中程式的行為。如果 Var 需要精確定義一次(這應該非常罕見),請考慮使用 defonce 進行定義。

在處理具有許多命名空間的程式碼庫時,重新載入適當的命名空間並按照正確的順序重新載入可能會很困難:tools.namespace 函式庫是用來協助程式設計師執行這項任務的。

程式狀態和原始碼應保持同步。你通常希望確保程式狀態反映你的原始碼,反之亦然,但這並非自動進行。重新載入程式碼通常還不夠:你還需要相應地轉換程式狀態。Alessandra Sierra 在她的文章 My Clojure Workflow, Reloaded 和她的演講 Components Just Enough Structure 中闡述了這個問題。

這促成了狀態管理函式庫的建立:

  • Component,它推廣一種程式狀態的表示方式,為稱為系統的 Clojure 記錄的受控映射。

  • System 是建立在 Component 上的函式庫,提供一組現成的元件。

  • Mount 採取與 Component 截然不同的方法,選擇使用 Vars 和命名空間作為狀態的支援基礎架構。[3]

  • Integrant 是較新的函式庫,它採用 Component 的方法,同時解決其一些已知的限制。


1。在撰寫本文時(2018 年 3 月),nREPL 是用於 REPL 編輯器整合最受歡迎的工具鏈
2。在廣為人知的 自動化測試 技術中也會發生類似的現象:雖然測試可以為程式設計師帶來許多價值,但它需要特別小心編寫「可測試」的程式碼。就像測試一樣,在撰寫 Clojure 程式碼時,REPL 不應是事後才想到的事。
3。在撰寫本文時,Clojure 社群對於這兩種方法的相對優缺點存在爭議。