;; 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 中为运行开发环境重复了很多手动步骤。
考虑在你的项目中创建一个“dev”命名空间(例如 myproject.dev
),在其中定义函数来自动化常见的开发任务(例如:启动本地 Web 服务器、运行数据库查询、开启/关闭电子邮件发送等)。
当我修改代码时,通常很难将这些更改反映到正在运行的程序中:我必须在 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 编辑器 都支持在 REPL 中评估代码而无需离开当前代码缓冲区的方式,从而减少了程序员需要进行的上下文切换次数。下面是它的外观(此示例中使用的编辑器是 Cursive)
提示:你可以将某些表达式包装在
|
以下是 REPL 集成提供的一些常见编辑器命令。所有主要的 Clojure 编辑器都支持大多数命令
将光标前的表单发送到 REPL:在当前文件的命名空间中,在 REPL 中评估光标前的表达式。这对于在当前命名空间的上下文中进行实验很有用。
将顶层表单发送到 REPL:在当前文件的命名空间中,评估光标当前所处的最大表达式,通常是 (defn …)
或 (def …)
表达式。这对于在命名空间中定义或重新定义 Var 很有用。
在 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 中进行调试时,我们通常希望手动重现程序自动执行的操作,即在函数体内评估一些表达式。为此,我们需要重新创建目标表达式的上下文:实现这一点的一个技巧是使用 def
定义与表达式使用的局部变量具有相同名称和值的 Var。下面的“物理学”示例说明了这种方法
(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 上进行调试的技术。
从头开始学习 Clojure 由 Aphyr 撰写,包含一篇关于调试的章节,介绍了 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
定义为一个 0 元函数,该函数使用 a
。示例
;;; 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 在她的文章 我的 Clojure 工作流程,重新加载 及其演讲 组件适度结构 中阐述了这个问题。
这促使了状态管理库的创建: