Clojure

REPLでのプログラミング:データ可視化

式を評価するたびに、REPLは結果のテキスト表現を表示します。これは、Read-Eval-Print-LoopのPrint部分です。ほとんどの場合、このテキスト表現はプログラマにとって十分に分かりやすいですが、特に大きく入れ子の深いデータ構造を扱う場合、読みづらくなることがあります。

幸いにも、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プログラミングでは、このような複雑なデータ構造の多くの例が提供されます。

ご覧のとおり、最後の式の結果は1行に圧縮されており、読みづらくなっています。

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* Varを設定して出力を切り詰めることができます。

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* Varを設定して表示するアイテム数を制限できます。

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=>

例外の調査

式の中には、評価しても結果を返さず、代わりに例外をスローするものがあります。例外をスローすることは、プログラムが「式の評価中に何か問題が発生し、対処方法が分からなかったのであきらめました」と言っているようなものです。

たとえば、0で数を除算すると例外がスローされます。

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

デフォルトでは、REPLは例外の2行のサマリーを出力します。最初の行はエラーフェーズ(実行、コンパイル、マクロ展開など)とその場所を報告します。2行目は原因を報告します。

多くの場合これで十分ですが、さらに多くの情報があります。

まず、例外のスタックトレースを可視化できます。つまり、誤った命令につながった一連の関数呼び出しです。スタックトレースは、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セッションでは、空のベクトルで関数を呼び出すと0を0で除算することになったことが分かります。

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の正規表現パターンの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で操作してvについてよりよく理解しようとしましょう。

typeancestorsを使用した型階層の表示

vの印刷表現は、そのことについて1つを教えてくれます。つまり、実行時型です。この場合は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が利用できない場合もあります。そのような場合は、Javaリフレクションを介してREPL自体を使用して型を検査できます。

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

たとえば、これにより、引数なしで.readメソッドをvに呼び出すことができ、intが返されることが分かります。

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

事前の知識なしに、vがInputStreamであり、そこからバイトを読み取ることができることを理解することができました。


1. たとえば、DatomicDataScriptのエンティティオブジェクトは、通常のマップとは大きく異なるにもかかわらず、Clojureマップのように出力されます。