Clojure

REPLでのプログラミング: REPLワークフローの強化

ここまでで、REPLの仕組みを理解するのに十分な知識が身につきました。ここでは、REPLを使用したより効率的な開発エクスペリエンスを提供することに焦点を当てます。改善したい点がいくつかあります。

エディタとREPLの切り替えが面倒です。

ほとんどのClojureプログラマーは、日常の開発にターミナルベースのREPLを使用しません。代わりに、エディタにREPL連携機能を使用します。これにより、エディタのバッファに直接式を記述し、ホットキー1つでREPLで評価できます。詳しくは、以下のエディタ連携セクションをご覧ください。

Clojureで小さな実験をしたいのですが、デフォルトのCLIツールでコードを書くのは大変です。

上記のように、1つの解決策はエディタ連携を使用することです。Nightcodeなどの一部のエディタは、Clojureの「バッテリー付属」エクスペリエンスを提供するように設計されていることに注意してください。

ただし、エディタのセットアップが面倒な場合は、より人間工学に基づいたターミナルREPLクライアントも存在します。

REPLから実行しているプログラムをデバッグする必要があります。

REPLは間違いなくその役に立ちます。以下のデバッグツールとテクニックセクションをご覧ください。

開発環境を実行するために、REPLで多くの手動手順を繰り返していることに気づきました。

プロジェクトに「dev」名前空間(例:myproject.dev)を作成し、そこで一般的な開発タスク(ローカルWebサーバーの起動、データベースクエリのの実行、メール送信のオン/オフなど)を自動化する関数を定義することを検討してください。

コードに変更を加えたときに、実行中のプログラムにその変更を反映させるのが難しい場合があります。変更を反映させるために、REPLで多くの手作業を行う必要があります。

プログラムを作成する際の選択に応じて、REPLでの操作のしやすさが変わります。以下のREPLフレンドリーなプログラムの作成セクションをご覧ください。

REPLセッションを「ノートブック」形式で保存したいです。

Gorilla REPL はまさにこの目的のために作られました.

REPLが提供するものよりも優れたデータの可視化が必要です.

特殊な Clojure エディタを使用すると、可視化機能が向上する可能性があります。以下の エディタ連携 セクションを参照してください.

とはいえ、REPL はフル機能の 実行環境であることに留意してください。特に、REPL を使用して特殊な可視化ツール (自分で開発したものも含む) を起動できます。例えば

  • RevealCognitect REBL は、Clojure データをナビゲートおよび可視化するためのグラフィカルツールであり、Clojure REPL との双方向のインタラクションをサポートしています.

  • oz は、数値チャートを表示するための Clojure ライブラリです.

  • datawalk は、複雑な Clojure データ構造をインタラクティブに探索するための Clojure ライブラリです.

  • system-viz は、実行中の Clojure システムのコンポーネントを可視化するための Clojure ライブラリです.

REPL をカスタマイズしたいです.

通常、REPL の読み取り、評価、および印刷方法をカスタマイズできますが、その方法はツールチェーンによって異なります。例えば

  • clojure.main から開始された REPL を使用する場合 (clj ツールを使用する場合と同様)、 'sub-REPL' を起動することで REPL をカスタマイズできます。 clojure.main/repl を参照してください.

  • nREPL を使用する場合[1]、カスタム ミドルウェア を記述することでこれを行うことができます.

REPL を使用してライブ本番システムに接続したいです.

Clojure の ソケットサーバー 機能をその目的で使用できます。 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 で評価します。 現在の名前空間のコンテキストで実験する場合に便利です.

  • **トップレベルのフォームを REPL に送信:** カーソルが現在含まれている最大の式 (通常は (defn …​) または (def …​) 式) を現在のファイルの名前空間で評価します。 名前空間で Vars を定義または再定義する場合に便利です.

  • **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)))

さらに詳しく: 'スパイ' マクロ

一部の 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 でデバッグを行う際、プログラムが自動的に行った処理、つまり関数本体内での式の評価を手動で再現したいことがよくあります。そのためには、対象となる式のコンテキストを再作成する必要があります。そのための方法の1つは、式で使用されているローカル変数と同じ名前と値を持つ Var を(`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 Debugging: No Stacktrace Required でより詳細に説明されています。scope-capture ライブラリは、式のコンテキストを保存および再作成する手動タスクを自動化するために作成されました。

REPL デバッグに関するコミュニティリソース

  • The Clojure Toolbox は、デバッグ用の Clojure ライブラリのリストを提供しています。

  • The Power of Clojure: debugging は、Cambium Consulting による記事で、REPL でのデバッグ手法のリストを提供しています。

  • Aphyr による Clojure From the Ground Up には、デバッグに関する章 があり、Clojure 特有のデバッグ手法と、一般的なデバッグへの原則的なアプローチが紹介されています。

  • Stuart Halloway は、REPL Debugging: No Stacktrace Required という記事で、REPL での迅速なフィードバックループを使用して、エラー情報を使用せずにバグの原因を絞り込む方法を説明しています。

  • Eli Bendersky は、Notes on debugging Clojure code を書いています。

  • Debugging with the Scientific Method は、Stuart Halloway によるカンファレンストークで、一般的なデバッグへの科学的アプローチを推奨しています。

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 とは根本的に異なるアプローチを採用し、状態のサポートインフラストラクチャとして Var と名前空間を使用することを選択しています。3

  • Integrant は、より新しいライブラリで、Component のアプローチを共有しながら、その認識されている制限事項に対処しています。


1. 本稿執筆時点(2018年3月)では、nREPL が REPL とエディタの統合のための最も一般的なツールチェーンです。
2. よく知られている 自動テスト の手法でも同様の現象が発生します。テストはプログラマに多くの価値をもたらしますが、「テスト可能」なコードを書くためには特別な注意が必要です。テストと同様に、REPL は Clojure コードを書く際の事後的な考慮事項であってはなりません。
3. 本稿執筆時点では、Clojure コミュニティでは、両方のアプローチの相対的なメリットについて議論が続いています。