Clojureで快適なREPL駆動開発のために"reloaded workflow"を実践しよう

ClojureといえばREPL駆動開発(REPL-driven development)によるインタラクティブで高速な開発スタイルが大きな強みですが、その開発体験(DX; developer experience)をより良くするためにClojure開発環境の特性を反映した"reloaded workflow"というものが知られています。

このワークフローを実現するためのライブラリの組み合わせや設定のしかたは多様ですが、本記事では最もオーソドックスな[tools.namespace](https://github.com/clojure/tools.namespace) + [Integrant](https://github.com/weavejester/integrant)/[Component](https://github.com/stuartsierra/component)/[mount](https://github.com/tolitius/mount)での構成例をご紹介します。

サンプルコード[lagenorhynque/reloaded-workflow-examples](https://github.com/lagenorhynque/reloaded-workflow-examples)は適宜参考に、ご自身の開発スタイルに合ったプロジェクト構成やエディタ設定をぜひ考えてみてください(お気に入りの構成が見つかったらプロジェクトテンプレート化しても良いかもしれません)。

# reloaded workflowとは

[Stuart Sierra](https://twitter.com/stuartsierra)による有名な記事[Clojure Workflow Reloaded](https://cognitect.com/blog/2013/06/04/clojure-workflow-reloaded)でモチベーションと実現方法が解説されています。

一言でいえば、REPLを再起動する(JVMの起動とClojure本体/プロジェクトの初回ロードのオーバーヘッドが無視できない😇)ことなくソースコードの変更をリロードしアプリケーションを安全に再起動できる仕組みを整えることにより、高速かつ整合的にREPL駆動開発を継続できるようにするワークフローのことです。

Clojureの準標準ライブラリのひとつである[tools.namespace](https://github.com/clojure/tools.namespace)の `refresh` 系関数を利用すると名前空間の依存関係に基づいてソースコードを賢くリロードすることができるわけですが、それに加えてIntegrant, Component, mountなど[アプリケーションの状態/ライフサイクルを管理するライブラリ](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba#%E7%8A%B6%E6%85%8B%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E7%AE%A1%E7%90%86%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA)を導入します。

この種のライフサイクル管理ライブラリでは、状態(state)を持つもの(「コンポーネント」と呼ばれる)に対する「起動」(リソースの初期化など)と「停止」(リソースの解放など)というライフサイクルを管理し、コンポーネントの組み合わせ(依存関係によるグラフ)によってアプリケーション(「システム」と呼ばれる)を構成します。

依存関係を整理し外部から注入するDI (dependency injection)の仕組みとして利用することもでき(ライフサイクル管理の対象であるコンポーネントの単位で差し替えが可能なため)、効果的に活用するとアプリケーションのアーキテクチャを疎結合に保つことにも繋がります。

例えば以下のようなコンポーネント(楕円)と依存関係(矢印)で成り立つアプリケーションがあったとすると、

![components.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/3c2327a8-838b-ac8d-0058-2675aadcedd2.png)

システム全体を起動するには

1. DB, external API
1. app handler
1. server

の順に初期化し、システム全体を停止するには

1. server
1. app handler
1. DB, external API

の順にリソースを解放します(停止時に操作が不要なコンポーネントもあります)。

# 実践してみる

サンプルコードリポジトリ: [lagenorhynque/reloaded-workflow-examples](https://github.com/lagenorhynque/reloaded-workflow-examples)

## 1. tools.namespaceとライフサイクル管理ライブラリを導入する

ここでは[Leiningen](https://leiningen.org/)管理のプロジェクトで例示しますが、[Clojure CLI](https://clojure.org/guides/deps_and_cli)管理のプロジェクトでもほぼ同等の設定が可能なはずです。

このあと実装するサンプルアプリケーションのために[Ring](https://github.com/ring-clojure/ring)関連のライブラリもあらかじめdependenciesに追加しておきます。

### Integrantの場合

- [Integrant](https://github.com/weavejester/integrant)
- [Integrant-REPL](https://github.com/weavejester/integrant-repl) ([tools.namespace](https://github.com/clojure/tools.namespace)を内包している)

```sh
$ lein new app integrant-app
```

- Leiningenプロジェクトの設定

```clojure:integrant-app/project.clj
(defproject integrant-app "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[integrant "0.8.0"]
                 [org.clojure/clojure "1.10.3"]
                 [ring/ring-core "1.9.3"]
                 [ring/ring-jetty-adapter "1.9.3"]]
  :main ^:skip-aot integrant-app.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}
             :dev {:source-paths ["dev/src"]
                   :resource-paths ["dev/resources"]
                   :dependencies [[integrant/repl "0.3.2"]]}
             :repl {:repl-options {:init-ns user}}})
```

- `user` 名前空間の定義

```clojure:integrant-app/dev/src/user.clj
(ns user)

(defn dev
  "Load and switch to the 'dev' namespace."
  []
  (require 'dev)
  (in-ns 'dev)
  :loaded)
```

- `dev` 名前空間の定義

```clojure:integrant-app/dev/src/dev.clj
(ns dev
  (:require
   [clojure.java.io :as io]
   [clojure.repl :refer :all]
   [clojure.spec.alpha :as s]
   [clojure.tools.namespace.repl :refer [refresh]]
   [integrant.core :as ig]
   [integrant.repl :refer [clear halt go init prep reset]]
   [integrant.repl.state :refer [config system]]))

(clojure.tools.namespace.repl/set-refresh-dirs "dev/src" "src" "test")
```

### Componentの場合

- [Component](https://github.com/stuartsierra/component)
- [component.repl](https://github.com/stuartsierra/component.repl) ([tools.namespace](https://github.com/clojure/tools.namespace)を内包している)

```sh
$ lein new app component-app
```

- Leiningenプロジェクトの設定

```clojure:component-app/project.clj
(defproject component-app "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[com.stuartsierra/component "1.0.0"]
                 [org.clojure/clojure "1.10.3"]
                 [ring/ring-core "1.9.3"]
                 [ring/ring-jetty-adapter "1.9.3"]]
  :main ^:skip-aot component-app.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}
             :dev {:source-paths ["dev/src"]
                   :resource-paths ["dev/resources"]
                   :dependencies [[com.stuartsierra/component.repl "0.2.0"]]}
             :repl {:repl-options {:init-ns user}}})
```

- `user` 名前空間の定義

```clojure:component-app/dev/src/user.clj
(ns user)

(defn dev
  "Load and switch to the 'dev' namespace."
  []
  (require 'dev)
  (in-ns 'dev)
  :loaded)
```

- `dev` 名前空間の定義

```clojure:component-app/dev/src/dev.clj
(ns dev
  (:require
   [clojure.java.io :as io]
   [clojure.repl :refer :all]
   [clojure.spec.alpha :as s]
   [clojure.tools.namespace.repl :refer [refresh]]
   [com.stuartsierra.component :as component]
   [com.stuartsierra.component.repl :refer [reset set-init start stop system]]))

(clojure.tools.namespace.repl/set-refresh-dirs "dev/src" "src" "test")
```

### mountの場合

- [mount](https://github.com/tolitius/mount)
- [tools.namespace](https://github.com/clojure/tools.namespace)

```sh
$ lein new app mount-app
```

- Leiningenプロジェクトの設定

```clojure:mount-app/project.clj
(defproject mount-app "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[mount "0.1.16"]
                 [org.clojure/clojure "1.10.3"]
                 [ring/ring-core "1.9.3"]
                 [ring/ring-jetty-adapter "1.9.3"]]
  :main ^:skip-aot mount-app.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}
             :dev {:source-paths ["dev/src"]
                   :resource-paths ["dev/resources"]
                   :dependencies [[org.clojure/tools.namespace "1.1.0"]]}
             :repl {:repl-options {:init-ns user}}})
```

- `user` 名前空間の定義

```clojure:mount-app/dev/src/user.clj
(ns user)

(defn dev
  "Load and switch to the 'dev' namespace."
  []
  (require 'dev)
  (in-ns 'dev)
  :loaded)
```

- `dev` 名前空間の定義

```clojure:mount-app/dev/src/dev.clj
(ns dev
  (:require
   [clojure.java.io :as io]
   [clojure.repl :refer :all]
   [clojure.spec.alpha :as s]
   [clojure.tools.namespace.repl :refer [refresh]]
   [mount.core :as mount :refer [start stop]]))

(clojure.tools.namespace.repl/set-refresh-dirs "dev/src" "src" "test")

(defn reset []
  (stop)
  (refresh :after 'mount.core/start))
```

## 2. アプリケーションの基礎を構築する

ここでは[Ring](https://github.com/ring-clojure/ring)ベースのWebサーバをミニマルに実装してみましょう。

"server"(Webサーバ)と"app"(アプリケーションの起点となるRingのハンドラ関数)という2つの「コンポーネント」による「システム」を考えます。

![minimal-components.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/48c66eb5-849a-2a7f-f022-7e8f611bde09.png)

### Integrantの場合

- `::app` コンポーネントの実装

```clojure:integrant-app/src/integrant_app/handler.clj
(ns integrant-app.handler
  (:require
   [integrant.core :as ig]
   [ring.util.response :as response]))

(defn hello-world [_]
  (response/response "Hello, World!"))

(defmethod ig/init-key ::app [_ _]
  hello-world)
```

- `::server` コンポーネントの実装

```clojure:integrant-app/src/integrant_app/server.clj
(ns integrant-app.server
  (:require
   [integrant.core :as ig]
   [ring.adapter.jetty :as jetty]))

(defmethod ig/init-key ::server [_ {:keys [app options]}]
  (jetty/run-jetty app options))

(defmethod ig/halt-key! ::server [_ server]
  (.stop server))
```

- システムのもとになる設定マップと `-main` 関数の定義

```clojure:integrant-app/src/integrant_app/core.clj
(ns integrant-app.core
  (:gen-class)
  (:require
   [integrant-app.handler :as handler]
   [integrant-app.server :as server]
   [integrant.core :as ig]))

(def config
  {::handler/app {}
   ::server/server {:app (ig/ref ::handler/app)
                    :options {:port 3000
                              :join? false}}})

(defn -main [& _]
  (-> config
      ig/prep
      ig/init))
```

※ 設定マップ `config` はここではマップとしてトップレベル定義しているが、[edn](https://github.com/edn-format/edn)ファイルとして外部化して読み込ませることが多い

- REPLからシステムのライフサイクルを操作するための初期設定

```diff:integrant-app/dev/src/dev.clj
@@ -4,8 +4,11 @@
    [clojure.repl :refer :all]
    [clojure.spec.alpha :as s]
    [clojure.tools.namespace.repl :refer [refresh]]
+   [integrant-app.core]
    [integrant.core :as ig]
    [integrant.repl :refer [clear halt go init prep reset]]
    [integrant.repl.state :refer [config system]]))

 (clojure.tools.namespace.repl/set-refresh-dirs "dev/src" "src" "test")
+
+(integrant.repl/set-prep! (comp ig/prep (constantly integrant-app.core/config)))
```

### Componentの場合

- `App` コンポーネントの実装

```clojure:component-app/src/component_app/handler.clj
(ns component-app.handler
  (:require
   [com.stuartsierra.component :as component]
   [ring.util.response :as response]))

(defn hello-world [_]
  (response/response "Hello, World!"))

(defrecord App
  [handler]
  component/Lifecycle
  (start [this]
    (assoc this :handler hello-world))
  (stop [this]
    this))
```

- `Server` コンポーネントの実装

```clojure:component-app/src/component_app/server.clj
(ns component-app.server
  (:require
   [com.stuartsierra.component :as component]
   [ring.adapter.jetty :as jetty]))

(defrecord Server
  [instance app options]
  component/Lifecycle
  (start [this]
    (cond-> this
      (not instance) (assoc :instance (jetty/run-jetty (:handler app) options))))
  (stop [this]
    (when instance
      (.stop instance))
    (assoc this :instance nil)))
```

- システムマップを初期化する関数と `-main` 関数の定義

```clojure:component-app/src/component_app/core.clj
(ns component-app.core
  (:gen-class)
  (:require
   [com.stuartsierra.component :as component]
   [component-app.handler :as handler]
   [component-app.server :as server]))

(defn init-system [{:keys [server]}]
  (component/system-map
   :app (handler/map->App {})
   :server (component/using (server/map->Server
                             (assoc server
                                    :options {:port 3000
                                              :join? false}))
                            [:app])))

(defn -main [& _]
  (component/start (init-system nil)))
```

※ ここではシステムマップの構築時にサーバの設定(`:options` キーの値)をハードコードして与えているが、実用的なアプリケーションでは外部から設定を注入することが多い

- REPLからシステムのライフサイクルを操作するための初期設定

```diff:component-app/dev/src/dev.clj
@@ -5,6 +5,9 @@
    [clojure.spec.alpha :as s]
    [clojure.tools.namespace.repl :refer [refresh]]
    [com.stuartsierra.component :as component]
-   [com.stuartsierra.component.repl :refer [reset set-init start stop system]]))
+   [com.stuartsierra.component.repl :refer [reset set-init start stop system]]
+   [component-app.core]))

 (clojure.tools.namespace.repl/set-refresh-dirs "dev/src" "src" "test")
+
+(set-init component-app.core/init-system)
```

### mountの場合

- `app` コンポーネントの実装

```clojure:mount-app/src/mount_app/handler.clj
(ns mount-app.handler
  (:require
   [mount.core :refer [defstate]]
   [ring.util.response :as response]))

(defn hello-world [_]
  (response/response "Hello, World!"))

(defstate app
  :start hello-world)
```

- `env` コンポーネントの実装

```clojure:mount-app/src/mount_app/config.clj
(ns mount-app.config
  (:require
   [mount.core :refer [defstate]]))

(defstate env
  :start {:server {:options {:port 3000
                             :join? false}}})
```

※ mount版ではサーバの設定を独立したコンポーネントとした(ここではマップとしてハードコードしているが、実用的なアプリケーションでは環境変数や設定ファイルから読み込んだデータで初期化することが多い)

- `server` コンポーネントの実装

```clojure:mount-app/src/mount_app/server.clj
(ns mount-app.server
  (:require
   [mount-app.config :as config]
   [mount-app.handler :as handler]
   [mount.core :refer [defstate]]
   [ring.adapter.jetty :as jetty]))

(defstate server
  :start (jetty/run-jetty handler/app (-> config/env :server :options))
  :stop (.stop server))
```

- `-main` 関数の定義

```clojure:mount-app/src/mount_app/core.clj
(ns mount-app.core
  (:gen-class)
  (:require
   [mount-app.server]
   [mount.core :as mount]))

(defn -main [& _]
  (mount/start))
```

## 3. アプリケーションを素早くリロード & (再)起動する

ここまでの準備が上手くいっていれば、REPLを起動して `(dev)` で `dev` 名前空間をロードして移動した後に `(reset)` すると、ソースコードをリロードしてアプリケーションを(再)起動することができます。

```clojure
user=> (dev)
2021-05-04 20:51:49.248:INFO::nREPL-session-13faeaf0-5180-4e36-afe6-613a14251abe: Logging initialized @7672ms to org.eclipse.jetty.util.log.StdErrLog
:loaded
dev=> (reset)
:reloading (integrant-app.server integrant-app.handler integrant-app.core integrant-app.core-test dev user)
2021-05-04 20:51:53.162:INFO:oejs.Server:nREPL-session-13faeaf0-5180-4e36-afe6-613a14251abe: jetty-9.4.40.v20210413; built: 2021-04-13T20:42:42.668Z; git: b881a572662e1943a14ae12e7e1207989f218b74; jvm 16.0.1+9
2021-05-04 20:51:53.201:INFO:oejs.AbstractConnector:nREPL-session-13faeaf0-5180-4e36-afe6-613a14251abe: Started ServerConnector@230c9323{HTTP/1.1, (http/1.1)}{0.0.0.0:3000}
2021-05-04 20:51:53.201:INFO:oejs.Server:nREPL-session-13faeaf0-5180-4e36-afe6-613a14251abe: Started @11625ms
:resumed
```

特に `(reset)` 操作は開発を進めるたびにリロード & (再)起動のために非常に高頻度で利用するので、以下のようにエディタ/IDEから簡単に実行できるようにしておくと便利です。

### [CIDER](https://cider.mx/) (Emacs, Spacemacs)

[Miscellaneous Features :: CIDER Docs > Reloading Code](https://docs.cider.mx/cider/usage/misc_features.html#reloading-code)にあるように、 `cider-ns-refresh` コマンド(≒ clojure.tools.namespaceの `refresh` 系関数呼び出し)の前後に任意の関数をフックすることができます。

`(reset)` 相当の振る舞い(Integrantでは `suspend` -> `refresh` -> `resume`)になるように、プロジェクト固有の `.dir-locals.el` に次のように設定しましょう。

ちなみにSpacemacs ([Clojure layer](https://github.com/syl20bnr/spacemacs/tree/develop/layers/+lang/clojure))のEvilキーバインドでは `, e n r` で `cider-ns-refresh` が呼び出せます。

- Integrantの場合

```emacs-lisp:integrant-app/.dir-locals.el
((nil . ((cider-ns-refresh-before-fn . "integrant.repl/suspend")
         (cider-ns-refresh-after-fn  . "integrant.repl/resume"))))
```

- Componentの場合

```emacs-lisp:component-app/.dir-locals.el
((nil . ((cider-ns-refresh-before-fn . "com.stuartsierra.component.repl/stop")
         (cider-ns-refresh-after-fn  . "com.stuartsierra.component.repl/start"))))
```

- mountの場合

```emacs-lisp:mount-app/.dir-locals.el
((nil . ((cider-ns-refresh-before-fn . "mount.core/stop")
         (cider-ns-refresh-after-fn  . "mount.core/start"))))
```

### [Cursive](https://cursive-ide.com/) (IntelliJ IDEA)

[Cursive: The REPL > REPL commands](https://cursive-ide.com/userguide/repl.html#repl-commands)を参考に、以下のようにREPL commandを設定しましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/e7ad2112-a8a2-04de-a31c-520262d63f1a.png)

キーボードショートカットも設定しておくと素早く繰り返し実行できます(一例として `Option+R` に割り当て)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33430/f9a9d57b-f0d4-7019-4b9f-edc706ccf0d2.png)

### [Calva](https://calva.io/) (VS Code)

[Custom REPL Commands - Calva User Guide](https://calva.io/custom-commands/)を参考に、以下のようにREPL commandを設定しましょう。

この例では `Control+Option+Space R Enter` で実行することができます。

```json:settings.json
{
  ...,
  "calva.customREPLCommandSnippets": [
    {
      "name": "Reset system",
      "key": "r",
      "snippet": "(reset)",
      "ns": "dev",
      "repl": "clj"
    }
  ]
}
```

### [vim-iced](https://liquidz.github.io/vim-iced/) (Vim)

[vim-iced > 23.4. Reloaded workflows](https://liquidz.github.io/vim-iced/#_reloaded_workflows)にあるように、 `IcedRefresh` コマンド(≒ clojure.tools.namespaceの `refresh` 系関数呼び出し)の前後に任意の関数をフックすることができます。

`(reset)` 相当の振る舞い(Integrantでは `suspend` -> `refresh` -> `resume`)になるように、プロジェクト固有の `.vimrc` に次のように設定しましょう。

- Integrantの場合

```vimscript:integrant-app/.vimrc
let g:iced#nrepl#ns#refresh_before_fn = 'integrant.repl/suspend'
let g:iced#nrepl#ns#refresh_after_fn = 'integrant.repl/resume'
```

- Componentの場合

```vimscript:component-app/.vimrc
let g:iced#nrepl#ns#refresh_before_fn = 'com.stuartsierra.component.repl/stop'
let g:iced#nrepl#ns#refresh_after_fn = 'com.stuartsierra.component.repl/start'
```

- mountの場合

```vimscript:mount-app/.vimrc
let g:iced#nrepl#ns#refresh_before_fn = 'mount.core/stop'
let g:iced#nrepl#ns#refresh_after_fn = 'mount.core/start'
```

また、 `(dev)` 操作については `IcedEval` で手軽に実行できるようにキーマップを設定しておくと便利かもしれません。

この例では `<Leader>gd` で `(dev)` できるようにしています。

```vimscript:~/.vimrc
aug MyClojureSetting
  au!
  au FileType clojure nnoremap <buffer> <Leader>gd :<C-u>IcedEval (user/dev)<CR>
aug END
```

# Further Reading

## Clojure開発環境

- [Clojure/ClojureScript関連リンク集 - Qiita > エディタプラグイン](https://qiita.com/lagenorhynque/items/68c314c288b75a9492ba#%E3%82%A8%E3%83%87%E3%82%A3%E3%82%BF%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3)
- [Clojure開発環境での基本操作まとめ: Spacemacs, IntelliJ IDEA (Cursive), VS Code (Calva), Vim (vim-iced), rebel-readline - Qiita](https://qiita.com/lagenorhynque/items/0b52575e90bad563e4bd)
- [Clojure開発環境でのリンターclj-kondo, Joker設定まとめ: Spacemacs, IntelliJ IDEA (Cursive), VS Code (Calva), Vim (vim-iced) - Qiita](https://qiita.com/lagenorhynque/items/dd9d6a1d97cbea738bc0)
- [Clojure開発環境でのフォーマッターcljstyle設定まとめ: Spacemacs, IntelliJ IDEA (Cursive), VS Code (Calva), Vim (vim-iced) - Qiita](https://qiita.com/lagenorhynque/items/a5d83b4a36a1cf1cacbe)

## Integrantの拡張としてのDuct

- [ClojureサーバサイドフレームワークDuctガイド - Qiita](https://qiita.com/lagenorhynque/items/8201c116c87b40eb9c22)
- [ClojureのDuctとClojureScriptのre-frameによるREST API + SPA開発入門 - Qiita](https://qiita.com/lagenorhynque/items/38537fa91300e0ac0070)