Clojure

REPL编程:数据可视化

每次我们计算一个表达式时,REPL都会向我们显示结果的文本表示形式:这就是Read-Eval-Print-LoopPrint部分。大多数情况下,这种文本表示形式对于程序员来说足够清晰,但有时它会变得难以阅读——尤其是在处理大型或深度嵌套的数据结构时。

幸运的是,REPL提供了更强大的数据可视化工具,我们将在本章中介绍这些工具。

使用clojure.pprint进行漂亮打印

例如,考虑以下代码,它计算了一些数字的算术属性的摘要

user=> (defn number-summary
  "Computes a summary of the arithmetic properties of a number, as a data structure."
  [n]
  (let [proper-divisors (into (sorted-set)
                          (filter
                            (fn [d]
                              (zero? (rem n d)))
                            (range 1 n)))
        divisors-sum (apply + proper-divisors)]
    {:n n
     :proper-divisors proper-divisors
     :even? (even? n)
     :prime? (= proper-divisors #{1})
     :perfect-number? (= divisors-sum n)}))
#'user/number-summary
user=> (mapv number-summary [5 6 7 12 28 42])
[{:n 5, :proper-divisors #{1}, :even? false, :prime? true, :perfect-number? false} {:n 6, :proper-divisors #{1 2 3}, :even? true, :prime? false, :perfect-number? true} {:n 7, :proper-divisors #{1}, :even? false, :prime? true, :perfect-number? false} {:n 12, :proper-divisors #{1 2 3 4 6}, :even? true, :prime? false, :perfect-number? false} {:n 28, :proper-divisors #{1 2 4 7 14}, :even? true, :prime? false, :perfect-number? true} {:n 42, :proper-divisors #{1 2 3 6 7 14 21}, :even? true, :prime? false, :perfect-number? false}]
user=>

目前,您不需要理解上面定义的number-summary函数的代码:我们只是使用它作为合成一些复杂数据结构的借口。特定领域的真实世界Clojure编程将为您提供许多此类复杂数据结构的示例。

如我们所见,我们最后一个表达式的结果被压缩在一行上,这使得它难以阅读

user=> (mapv number-summary [5 6 7 12 28 42])
[{:n 5, :proper-divisors #{1}, :even? false, :prime? true, :perfect-number? false} {:n 6, :proper-divisors #{1 2 3}, :even? true, :prime? false, :perfect-number? true} {:n 7, :proper-divisors #{1}, :even? false, :prime? true, :perfect-number? false} {:n 12, :proper-divisors #{1 2 3 4 6}, :even? true, :prime? false, :perfect-number? false} {:n 28, :proper-divisors #{1 2 4 7 14}, :even? true, :prime? false, :perfect-number? true} {:n 42, :proper-divisors #{1 2 3 6 7 14 21}, :even? true, :prime? false, :perfect-number? false}]

我们可以使用clojure.pprint库以更“视觉化”的格式打印结果

user=> (require '[clojure.pprint :as pp])
nil
user=> (pp/pprint (mapv number-summary [5 6 7 12 28 42]))
[{:n 5,
  :proper-divisors #{1},
  :even? false,
  :prime? true,
  :perfect-number? false}
 {:n 6,
  :proper-divisors #{1 2 3},
  :even? true,
  :prime? false,
  :perfect-number? true}
 {:n 7,
  :proper-divisors #{1},
  :even? false,
  :prime? true,
  :perfect-number? false}
 {:n 12,
  :proper-divisors #{1 2 3 4 6},
  :even? true,
  :prime? false,
  :perfect-number? false}
 {:n 28,
  :proper-divisors #{1 2 4 7 14},
  :even? true,
  :prime? false,
  :perfect-number? true}
 {:n 42,
  :proper-divisors #{1 2 3 6 7 14 21},
  :even? true,
  :prime? false,
  :perfect-number? false}]
nil

提示:使用编辑器突出显示结果的语法

如果您希望以更多视觉对比的方式显示漂亮打印的结果,您也可以将其复制到编辑器缓冲区(下面使用的编辑器是Emacs

Copying pretty-printed result to editor

需要漂亮打印最后一个REPL结果非常常见,因此clojure.pprint为此提供了一个函数:clojure.pprint/pp

user=> (mapv number-summary [12 28])
[{:n 12, :proper-divisors #{1 2 3 4 6}, :even? true, :prime? false, :perfect-number? false} {:n 28, :proper-divisors #{1 2 4 7 14}, :even? true, :prime? false, :perfect-number? true}]
user=> (pp/pp)
[{:n 12,
  :proper-divisors #{1 2 3 4 6},
  :even? true,
  :prime? false,
  :perfect-number? false}
 {:n 28,
  :proper-divisors #{1 2 4 7 14},
  :even? true,
  :prime? false,
  :perfect-number? true}]
nil

最后,对于结果是映射序列(如上所示)的情况,您可以使用clojure.pprint/print-table将其打印为表格

user=> (pp/print-table (mapv number-summary [6 12 28]))

| :n | :proper-divisors | :even? | :prime? | :perfect-number? |
|----+------------------+--------+---------+------------------|
|  6 |         #{1 2 3} |   true |   false |             true |
| 12 |     #{1 2 3 4 6} |   true |   false |            false |
| 28 |    #{1 2 4 7 14} |   true |   false |             true |
nil

截断REPL输出

当一个表达式计算结果为一个大型或深度嵌套的数据结构时,读取REPL输出可能会变得困难。

当结构嵌套过深时,您可以通过设置*print-level*变量来截断输出

user=> (set! *print-level* 3)
3
user=> {:a {:b [{:c {:d {:e 42}}}]}} ;; a deeply nested data structure
{:a {:b [#]}}

您可以通过计算(set! *print-level* nil)来撤消此设置。

同样地,当数据结构包含长集合时,您可以通过设置*print-length*变量来限制显示的项目数量

user=> (set! *print-length* 3)
3
user=> (repeat 100 (vec (range 100))) ;; a data structure containing looooong collections.
([0 1 2 ...] [0 1 2 ...] [0 1 2 ...] ...)

与上面一样,计算(set! *print-length* nil)来撤消此设置。

*print-level**print-length*会影响普通的REPL打印和漂亮打印。

访问最近的结果:*1*2*3

在REPL中,可以通过计算*1来检索最后计算的结果;之前的那个保存在*2中,再之前的那个保存在*3

user=> (mapv number-summary [6 12 28])
[{:n 6, :proper-divisors #{1 2 3}, :even? true, :prime? false, :perfect-number? true} {:n 12, :proper-divisors #{1 2 3 4 6}, :even? true, :prime? false, :perfect-number? false} {:n 28, :proper-divisors #{1 2 4 7 14}, :even? true, :prime? false, :perfect-number? true}]
user=> (pp/pprint *1) ;; using *1 instead of re-typing the previous expression (or its result)
[{:n 6,
 :proper-divisors #{1 2 3},
 :even? true,
 :prime? false,
 :perfect-number? true}
{:n 12,
 :proper-divisors #{1 2 3 4 6},
 :even? true,
 :prime? false,
 :perfect-number? false}
{:n 28,
 :proper-divisors #{1 2 4 7 14},
 :even? true,
 :prime? false,
 :perfect-number? true}]
nil
user=> *1 ;; now *1 has changed to become nil (because pp/pprint returns nil)
nil
user=> *3 ;; ... which now means that our initial result is in *3:
[{:n 6, :proper-divisors #{1 2 3}, :even? true, :prime? false, :perfect-number? true} {:n 12, :proper-divisors #{1 2 3 4 6}, :even? true, :prime? false, :perfect-number? false} {:n 28, :proper-divisors #{1 2 4 7 14}, :even? true, :prime? false, :perfect-number? true}]
user=>

提示:使用def定义结果来保存它

如果您想保留一个结果的时间超过3次计算,您可以简单地计算(def <some-name> *1)

user=> (mapv number-summary [6 12 28])
[{:n 6, :proper-divisors #{1 2 3}, :even? true, :prime? false ; ...
user=> (def my-summarized-numbers *1) ;; saving the result
#'user/my-summarized-numbers
user=> my-summarized-numbers
[{:n 6, :proper-divisors #{1 2 3}, :even? true, :prime? false ; ...
user=> (count my-summarized-numbers)
3
user=> (first my-summarized-numbers)
{:n 6, :proper-divisors #{1 2 3}, :even? true, :prime? false, ; ...
user=> (pp/print-table my-summarized-numbers)

| :n | :proper-divisors | :even? | :prime? | :perfect-number? |
|----+------------------+--------+---------+------------------|
|  6 |         #{1 2 3} |   true |   false |             true |
| 12 |     #{1 2 3 4 6} |   true |   false |            false |
| 28 |    #{1 2 4 7 14} |   true |   false |             true |
nil
user=>

调查异常

当您计算某些表达式时,它们不会返回结果,而是会抛出一个异常。抛出异常意味着您的程序在告诉您:“计算表达式时出现问题,我不知道如何处理它,所以我放弃了。”

例如,如果您将数字除以零,则会抛出异常

user=> (/ 1 0)
Execution error (ArithmeticException) at user/eval163 (REPL:1).
Divide by zero

默认情况下,REPL会打印异常的双行摘要。第一行报告错误阶段(执行、编译、宏扩展等)及其位置。第二行报告原因。

在许多情况下,这可能就足够了,但还有更多信息可用。

首先,您可以可视化异常的堆栈跟踪——也就是说,导致错误指令的函数调用链。可以使用clojure.repl/pst打印堆栈跟踪

user=> (pst *e)
ArithmeticException Divide by zero
	clojure.lang.Numbers.divide (Numbers.java:163)
	clojure.lang.Numbers.divide (Numbers.java:3833)
	user/eval15 (NO_SOURCE_FILE:3)
	user/eval15 (NO_SOURCE_FILE:3)
	clojure.lang.Compiler.eval (Compiler.java:7062)
	clojure.lang.Compiler.eval (Compiler.java:7025)
	clojure.core/eval (core.clj:3206)
	clojure.core/eval (core.clj:3202)
	clojure.main/repl/read-eval-print--8572/fn--8575 (main.clj:243)
	clojure.main/repl/read-eval-print--8572 (main.clj:243)
	clojure.main/repl/fn--8581 (main.clj:261)
	clojure.main/repl (main.clj:261)
nil

提示:可以通过计算*e来获取最后抛出的异常。

最后,只需在REPL中计算异常即可提供有用的可视化

user=> *e
#error {
 :cause "Divide by zero"
 :via
 [{:type java.lang.ArithmeticException
   :message "Divide by zero"
   :at [clojure.lang.Numbers divide "Numbers.java" 163]}]
 :trace
 [[clojure.lang.Numbers divide "Numbers.java" 163]
  [clojure.lang.Numbers divide "Numbers.java" 3833]
  [user$eval15 invokeStatic "NO_SOURCE_FILE" 3]
  [user$eval15 invoke "NO_SOURCE_FILE" 3]
  [clojure.lang.Compiler eval "Compiler.java" 7062]
  [clojure.lang.Compiler eval "Compiler.java" 7025]
  [clojure.core$eval invokeStatic "core.clj" 3206]
  [clojure.core$eval invoke "core.clj" 3202]
  [clojure.main$repl$read_eval_print__8572$fn__8575 invoke "main.clj" 243]
  [clojure.main$repl$read_eval_print__8572 invoke "main.clj" 243]
  [clojure.main$repl$fn__8581 invoke "main.clj" 261]
  [clojure.main$repl invokeStatic "main.clj" 261]
  [clojure.main$repl_opt invokeStatic "main.clj" 325]
  [clojure.main$main invokeStatic "main.clj" 424]
  [clojure.main$main doInvoke "main.clj" 387]
  [clojure.lang.RestFn invoke "RestFn.java" 397]
  [clojure.lang.AFn applyToHelper "AFn.java" 152]
  [clojure.lang.RestFn applyTo "RestFn.java" 132]
  [clojure.lang.Var applyTo "Var.java" 702]
  [clojure.main main "main.java" 37]]}

在这个简单的示例中,显示所有这些信息可能比诊断问题所需的多;但是对于“现实世界”中的异常,这种可视化会更有帮助,因为Clojure程序中的异常往往具有以下特征

  • 异常传达数据:在Clojure程序中,通常将其他数据附加到异常(不仅仅是人类可读的错误消息):这是通过使用clojure.core/ex-info创建异常来完成的。

  • 异常是链式结构:异常可以使用可选的原因进行注释,该原因是另一个(更低级别的)异常。

这是一个演示此类异常的示例程序。

(defn divide-verbose
  "Divides two numbers `x` and `y`, but throws more informative Exceptions when it goes wrong.
  Returns a (double-precision) floating-point number."
  [x y]
  (try
    (double (/ x y))
    (catch Throwable cause
      (throw
        (ex-info
          (str "Failed to divide " (pr-str x) " by " (pr-str y))
          {:numerator x
           :denominator y}
          cause)))))

(defn average
  "Computes the average of a collection of numbers."
  [numbers]
  (try
    (let [sum (apply + numbers)
          cardinality (count numbers)]
      (divide-verbose sum cardinality))
    (catch Throwable cause
      (throw
        (ex-info
          "Failed to compute the average of numbers"
          {:numbers numbers}
          cause)))))

我们还不知道,但是我们的average函数在应用于空的数字集合时会失败。但是,可视化异常使其易于诊断。在下面的REPL会话中,我们可以看到,使用空的数字向量调用我们的函数会导致将零除以零

user=> (average [])
Execution error (ArithmeticException) at user/divide-verbose (REPL:6).
Divide by zero
user=> *e  ;; notice the `:data` key inside the chain of Exceptions represented in `:via`
#error {
 :cause "Divide by zero"
 :via
 [{:type clojure.lang.ExceptionInfo
   :message "Failed to compute the average of numbers"
   :data {:numbers []}
   :at [user$average invokeStatic "NO_SOURCE_FILE" 10]}
  {:type clojure.lang.ExceptionInfo
   :message "Failed to divide 0 by 0"
   :data {:numerator 0, :denominator 0}
   :at [user$divide_verbose invokeStatic "NO_SOURCE_FILE" 9]}
  {:type java.lang.ArithmeticException
   :message "Divide by zero"
   :at [clojure.lang.Numbers divide "Numbers.java" 188]}]
 :trace
 [[clojure.lang.Numbers divide "Numbers.java" 188]
  [user$divide_verbose invokeStatic "NO_SOURCE_FILE" 6]
  [user$divide_verbose invoke "NO_SOURCE_FILE" 1]
  [user$average invokeStatic "NO_SOURCE_FILE" 7]
  [user$average invoke "NO_SOURCE_FILE" 1]
  [user$eval173 invokeStatic "NO_SOURCE_FILE" 1]
  [user$eval173 invoke "NO_SOURCE_FILE" 1]
  [clojure.lang.Compiler eval "Compiler.java" 7176]
  [clojure.lang.Compiler eval "Compiler.java" 7131]
  [clojure.core$eval invokeStatic "core.clj" 3214]
  [clojure.core$eval invoke "core.clj" 3210]
  [clojure.main$repl$read_eval_print__9068$fn__9071 invoke "main.clj" 414]
  [clojure.main$repl$read_eval_print__9068 invoke "main.clj" 414]
  [clojure.main$repl$fn__9077 invoke "main.clj" 435]
  [clojure.main$repl invokeStatic "main.clj" 435]
  [clojure.main$repl_opt invokeStatic "main.clj" 499]
  [clojure.main$main invokeStatic "main.clj" 598]
  [clojure.main$main doInvoke "main.clj" 561]
  [clojure.lang.RestFn invoke "RestFn.java" 397]
  [clojure.lang.AFn applyToHelper "AFn.java" 152]
  [clojure.lang.RestFn applyTo "RestFn.java" 132]
  [clojure.lang.Var applyTo "Var.java" 705]
  [clojure.main main "main.java" 37]]}

图形和基于Web的可视化

最后,REPL作为一个功能齐全的编程环境,它不仅限于基于文本的可视化。以下是与Clojure捆绑的一些方便的“图形”可视化工具

clojure.java.javadoc允许您查看类的Javadoc或对象。以下是如何查看Java 正则表达式Pattern的Javadoc

user=> (require '[clojure.java.javadoc :as jdoc])
nil
user=> (jdoc/javadoc #"a+") ;; opens the Javadoc page for java.util.Pattern in a Web browser
true
user=> (jdoc/javadoc java.util.regex.Pattern) ;; equivalent to the above
true

clojure.inspector允许您打开基于GUI的数据可视化,例如

user=> (require '[clojure.inspector :as insp])
nil
user=> (insp/inspect-table (mapv number-summary [2 5 6 28 42]))
#object[javax.swing.JFrame 0x26425897 "javax.swing.JFrame[frame1,0,23,400x600,layout=java.awt.BorderLayout,title=Clojure Inspector,resizable,normal,defaultCloseOperation=HIDE_ON_CLOSE,rootPane=javax.swing.JRootPane[,0,22,400x578,layout=javax.swing.JRootPane$RootLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=16777673,maximumSize=,minimumSize=,preferredSize=],rootPaneCheckingEnabled=true]"]

clojure.inspector table viz

clojure.java.browse/browse-url允许您在Web浏览器中打开任何URL,这对于特定需求可能很方便。

最后,还存在用于数据可视化的第三方Clojure工具;我们将在第增强REPL工作流程章中看到其中的一些工具。

处理神秘值(高级)

有时,REPL中值的打印表示形式信息量不大;有时,它甚至可能误导了该值的本质。[1] 这通常发生在通过Java互操作获得的值上。

例如,我们将使用clojure.java.io库创建一个InputStream对象。如果您不知道什么是InputStream,那就更好了——本节的重点是教您如何在未知领域找到立足点

user=> (require '[clojure.java.io :as io])
nil
user=> (def v (io/input-stream "https://www.clojure.org")) ;; NOTE won't work if you're not connected to the Internet
#'user/v
user=> v
#object[java.io.BufferedInputStream 0x4ee37ca3 "java.io.BufferedInputStream@4ee37ca3"]

以上代码示例定义了一个名为v的InputStream。

现在假设您不知道v来自哪里,让我们尝试在REPL中与它交互以对其有更多了解。

使用typeancestors查看类型层次结构

v的打印表示形式告诉我们关于它的一件事:它的运行时类型,在本例中为java.io.BufferedInputStream。值的类型可以帮助我们了解可以对其调用哪些操作。我们可以计算(type v)以获取v具体类型,并计算(ancestors (type v))以获取其整个类型层次结构:

user=> (type v) ;; what is the type of our obscure value?
java.io.BufferedInputStream
user=> (ancestors (type v))
#{java.io.InputStream java.lang.AutoCloseable java.io.Closeable java.lang.Object java.io.FilterInputStream}

使用Javadoc

如前所述,我们可以使用clojure.java.javadoc库查看有关Java类型的在线文档

user=> (require '[clojure.java.javadoc :as jdoc])
nil
user=> (jdoc/javadoc java.io.InputStream) ;; should open a web page about java.io.InputStream
true

使用clojure.reflect检查Java类型

Javadoc很有用,但有时Javadoc甚至不可用。在这种情况下,我们可以使用REPL本身通过Java反射来检查类型。

我们可以使用clojure.reflect/reflect函数获取有关Java类型的详细信息,作为简单的Clojure数据结构

user=> (require '[clojure.reflect :as reflect])
nil
user=> (reflect/reflect java.io.InputStream)
{:bases #{java.lang.Object java.io.Closeable}, :flags #{:public :abstract}, :members #{#clojure.reflect.Method{:name close, :return-type void, :declaring-class java.io.InputStream, :parameter-types [], :exception-types [java.io.IOException], :flags #{:public}} #clojure.reflect.Method{:name mark, :return-type void, :declaring-class java.io.InputStream, :parameter-types [int], :exception-types [], :flags #{:public :synchronized}} #clojure.reflect.Method{:name available, :return-type int, :declaring-class java.io.InputStream, :parameter-types [], :exception-types [java.io.IOException], :flags #{:public}} #clojure.reflect.Method{:name read, :return-type int, :declaring-class java.io.InputStream, :parameter-types [], :exception-types [java.io.IOException], :flags #{:public :abstract}} #clojure.reflect.Method{:name markSupported, :return-type boolean, :declaring-class java.io.InputStream, :parameter-types [], :exception-types [], :flags #{:public}} #clojure.reflect.Field{:name MAX_SKIP_BUFFER_SIZE, :type int, :declaring-class java.io.InputStream, :flags #{:private :static :final}} #clojure.reflect.Constructor{:name java.io.InputStream, :declaring-class java.io.InputStream, :parameter-types [], :exception-types [], :flags #{:public}} #clojure.reflect.Method{:name read, :return-type int, :declaring-class java.io.InputStream, :parameter-types [byte<>], :exception-types [java.io.IOException], :flags #{:public}} #clojure.reflect.Method{:name skip, :return-type long, :declaring-class java.io.InputStream, :parameter-types [long], :exception-types [java.io.IOException], :flags #{:public}} #clojure.reflect.Method{:name reset, :return-type void, :declaring-class java.io.InputStream, :parameter-types [], :exception-types [java.io.IOException], :flags #{:public :synchronized}} #clojure.reflect.Method{:name read, :return-type int, :declaring-class java.io.InputStream, :parameter-types [byte<> int int], :exception-types [java.io.IOException], :flags #{:public}}}}

现在,这是一个非常复杂的数据结构。幸运的是,我们已经学习了如何在本章的第一部分中处理复杂的数据结构:漂亮打印来救援!让我们使用漂亮打印以表格形式显示java.io.InputStream公开的方法

user=> (->> (reflect/reflect java.io.InputStream) :members (sort-by :name) (pp/print-table [:name :flags :parameter-types :return-type]))

|                :name |                     :flags | :parameter-types | :return-type |
|----------------------+----------------------------+------------------+--------------|
| MAX_SKIP_BUFFER_SIZE | #{:private :static :final} |                  |              |
|            available |                 #{:public} |               [] |          int |
|                close |                 #{:public} |               [] |         void |
|  java.io.InputStream |                 #{:public} |               [] |              |
|                 mark |   #{:public :synchronized} |            [int] |         void |
|        markSupported |                 #{:public} |               [] |      boolean |
|                 read |       #{:public :abstract} |               [] |          int |
|                 read |                 #{:public} |         [byte<>] |          int |
|                 read |                 #{:public} | [byte<> int int] |          int |
|                reset |   #{:public :synchronized} |               [] |         void |
|                 skip |                 #{:public} |           [long] |         long |
nil

例如,这告诉我们可以在v上调用一个不带参数的.read方法,该方法将返回一个int

user=> (.read v)
60
user=> (.read v)
33
user=> (.read v)
68

在没有任何先验知识的情况下,我们已经设法了解到v是一个InputStream,并从中读取字节。


1。例如,DatomicDataScript实体对象被打印为Clojure映射,即使它们与普通映射有很大不同。