;; 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 時擁有更順暢的開發體驗。有許多方面可以改善
在編輯器和 REPL 之間切換很麻煩。
大多數 Clojure 程式設計師並不會在日常開發中使用基於終端的 REPL:他們會在編輯器中使用 REPL 整合,這讓他們可以直接在編輯器緩衝區中撰寫表達式,並使用一個熱鍵在 REPL 中評估這些表達式。請參閱下方的 編輯器整合 部分,以取得更多詳細資訊。
我想在 Clojure 中進行小型實驗,但在預設的 CLI 工具中撰寫程式碼很痛苦。
但是,如果設定編輯器對你的品味來說太繁重,還有更符合人體工學的終端機 REPL 程式。
rebel-readline是 Bruce Hauman 製作的終端機 readline 函式庫。如果你已經安裝了Clojure CLI 工具,你可以在終端機中一行啟動它,而無需任何額外的安裝步驟。
我需要偵錯從 REPL 執行的程式。
REPL 絕對可以幫助你執行此操作:請參閱下方的偵錯工具和技術區段。
我發現自己在 REPL 中重複許多手動步驟來執行我的開發環境。
考慮在你的專案中建立一個「開發」命名空間(例如 myproject.dev
),在其中定義用於自動化常見開發任務的函式(例如:啟動本機網路伺服器、執行資料庫查詢、開啟/關閉電子郵件傳送等)。
當我對程式碼進行變更時,通常很難在我的執行程式中反映該變更:我必須在 REPL 中進行大量手動工作才能實現它。
根據你在撰寫程式時所做的選擇,在 REPL 中與它們互動將變得或多或少實用。請參閱下方的撰寫 REPL 友善程式區段。
我想以「筆記本」格式儲存我的 REPL 會話。
Gorilla REPL是為這個目的而製作的。
我想要比 REPL 提供的更好的資料視覺化。
您可能會從專業的 Clojure 編輯器中獲得改進的視覺化功能:請參閱以下 編輯器整合 部分。
話雖如此,請記住 REPL 是個具備完整功能的執行環境:特別是,您可以使用它來啟動特殊用途的視覺化工具(包括您自己開發的工具)。例如
Reveal 和 Cognitect REBL 是用於瀏覽和視覺化 Clojure 資料的圖形工具,支援與 Clojure REPL 的雙向互動。
oz 是個 Clojure 函式庫,用於顯示數值圖表
datawalk 是個 Clojure 函式庫,用於互動式探索複雜的 Clojure 資料結構
system-viz 是個 Clojure 函式庫,用於視覺化正在執行的 Clojure 系統的組成部分
我想自訂我的 REPL。
您通常可以自訂 REPL 的讀取、評估和列印方式,但執行方法取決於您的工具鏈。例如
當使用從 clojure.main 啟動的 REPL(例如使用 clj
工具時)時,您可以透過啟動「子 REPL」來自訂 REPL:請參閱 clojure.main/repl。
我想使用 REPL 連線到實際的生產系統。
Clojure socket 伺服器 功能可用於此目的。nREPL 和 unrepl 等工具可用於提供更豐富的體驗。
注意:您可能不需要所有這些! 根據您的專案和個人喜好,您很可能會只使用本部分中介紹的工具和技術的一小部分。了解這些選項的存在很重要,但不要嘗試一次全部採用! |
所有 主要的 Clojure 編輯器 都支援一種在 REPL 中評估程式碼的方式,而無需離開目前的程式碼緩衝區,這減少了程式設計人員必須執行的內容切換數量。以下是它的外觀(此範例中使用的編輯器是 Cursive)
提示:您可以將一些表達式包覆在
|
以下是 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 與
|
有些 Clojure 函式庫提供了 prn
的'加強'版本,這些版本更具資訊性,因為它們也會列印關於包裝表達式的資訊。例如
tools.logging 記錄函式庫提供了一個 spy 巨集,用於記錄表達式的程式碼及其值
spyscope 函式庫讓你能夠使用非常簡潔的語法插入這些列印呼叫。
追蹤函式庫,例如 tools.trace 和 Sayid,可以幫助你設定較大區塊的程式碼,例如自動列印給定命名空間中的所有函式呼叫,或給定表達式中的所有中間值。
有時你想要對中間值做更多的事,而不只是列印它們:你想要儲存它們,以便在 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 函式庫是用來自動化儲存和重新建立表達式背景的手動任務。
Clojure 工具箱 提供了一個用於除錯的 Clojure 函式庫清單。
Clojure 的力量:除錯 是 Cambium Consulting 的一篇文章,其中提供了一系列在 REPL 中除錯的技術。
Aphyr 的《Clojure 從頭開始》包含一個 關於除錯的章節,介紹了特別針對 Clojure 除錯的技術,以及一般除錯的原則性方法。
Stuart Halloway 在他的文章 REPL 除錯:不需要堆疊追蹤 中展示了如何使用 REPL 中的快速回饋迴路來縮小錯誤原因的範圍,而完全不使用錯誤資訊。
Eli Bendersky 寫了一些 關於除錯 Clojure 程式碼的筆記。
使用科學方法除錯 是 Stuart Halloway 的一場會議演講,宣傳了一種對一般除錯採取科學方法。
在 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 失效的問題可能會透過以下方式得到令人滿意的緩解
確保 Var 沒有衍生到不同的檔案中,並且在變更時重新載入整個檔案;
或使用 clojure.tools.namespace 等工具程式,讓你追蹤變更的檔案並依序重新載入它們。
對 REPL 友善的程式碼可以重新載入。確保重新載入命名空間不會改變執行中程式的行為。如果 Var 需要精確定義一次(這應該非常罕見),請考慮使用 defonce
進行定義。
在處理具有許多命名空間的程式碼庫時,重新載入適當的命名空間並按照正確的順序重新載入可能會很困難:tools.namespace 函式庫是用來協助程式設計師執行這項任務的。
程式狀態和原始碼應保持同步。你通常希望確保程式狀態反映你的原始碼,反之亦然,但這並非自動進行。重新載入程式碼通常還不夠:你還需要相應地轉換程式狀態。Alessandra Sierra 在她的文章 My Clojure Workflow, Reloaded 和她的演講 Components Just Enough Structure 中闡述了這個問題。
這促成了狀態管理函式庫的建立: