📝Clojure Integrant

📝Clojure Integrant

Clojure: Integrantとは #

データ駆動設計によるアプリケーションを作成するためのマイクロフレームワーク.

Dependency Ingection をClojureで実現.

設定データに対する初期化関数を定義でき, 設定データの定義から実体を生成.

Usage #

  • integrant
    • configuration map をもとに生成されるmicro-serviceの1つの単位.
  • configuration map
    • keyの定義をまずする. これは具体的な実装へと初期化される入力情報.
    • Clojureのmapとして表現するかEDNファイルとして外部ファイルに定義する.
    • configuration map同士は ig/ref で 参照することができる.
  • ig/init-key で integrant serviceの初期化におけるデータと関数を定義.
  • ig/halt-key!でintegrant serviceのinit-keyの定義を破棄する関数を定義.
  • ig/initにconfiguration mapを渡すことで, 依存関係に従って integrant を初期化.
  • init/halt!で 破棄.
(defmethod ig/init-key :handler/greet [_ {:keys [name]}]
  (fn [_] (resp/response (str "Hello " name))))
  • {:keys [name]}はClojure 分配束縛の記法.
  • keyに ::hogeみたいな 2つのコロンをみかける. ::hogeは :(namespace)/hogeの意味.
    • REPLで評価するとわかる, reader syntax.

Usage: Integrant suspend/resume #

https://github.com/weavejester/integrant#suspending-and-resuming

Integrantはinitとhalt, つまりシステムの開始と終了の機能を提供する.

suspend/resumeは主に 開発用 である. そして使いこなすにはatomとdelayをつかうというひと工夫を加える.

Integrantの考え方としてnamespaceにatomをbindingしない. その代わりに init-keyの中で atomを宣言して返り値のmapにbindingする.

Integrant-REPL #

IntegrantでReloaded Workflowをするための補助ライブラリ. Integrantと同じweavejesterさんが作成.

https://github.com/weavejester/integrant-repl

コード自体は小さくシンプルなのでなにをやっているのかはソース見るのが早いかも.

💡 systemをreplからみる #

個人的に強いとおもうTips. integrant.replはintegrantで初期化したsystemを alter-var-root 変数にbindしている. すると, このsystemの変数を覗いてしまえば動作しているシステムの中身がスケスケのまるみえ.

integrantが要はアプリのなかでatomなどの状態をバラバラに宣言するのではなくて一つのrootとそのツリーにまとめましょうという設計なので, この中をみればシステムの全てが見れる.

(require '[integrant.repl.state :refer [config system]])

CIDER連携 #

Emacs CIDER で M-x cider-ns-refreshの前後にsuspendとresumeをhookさせると気軽にシステムをreset.

.dir-locals.elに以下を記載.

((clojure-mode . ((cider-ns-refresh-before-fn . "integrant.repl/suspend")
                  (cider-ns-refresh-after-fn  . "integrant.repl/resume"))))

💡Integrantトピック #

Ductは内部でintegrantをつかっているのでductで検索してもいい.

💡考察: Integrantで状態を管理するということ(as State Management) #

Clojureの世界では, 普通は状態をatomで管理する.

Integrantを導入することで, 各namespaceに散らばるatomで宣言された状態をsystemというひとつの状態に紐づけてまとめることができる. そしてこのツリー構造で状態を管理するからこそシステムの停止や再起動が用意にできる.

逆に言うと, Integrantを利用するということは, namespaceでatomを宣言しないということ.

Global storage と言われたりする. (ref. Systems in Clojure).


Integrant: how to store and access the running system? : Clojure

Systems in Integrant are intended to be autonomous.

Anyway, my point is that it seems to me Integrant still requires a whole app buy-in in the sense that, unlike with Mount, you have to thread all your state through a single entry-point.

systemはthreadで動作する再帰プロセス. しかしこれはIntegrantと言うよりも関数型プログラミングのイディオム.

Only constants should be global.

定数のみが参照可能であり状態は隠されているという考え(debug除く).

💡考察: Java Command Patternからのアナロジー #

クラスというものを単なる抽象データ構造と捉えると, クラスには属性としての値と関数値の集合であり, オブジェクトとはそれをメモリ上に領域確保した状態.

ig/init-keyでやっていることは値とその初期化関数のpairのbindingであり, ig/init-keyで定義したpairの集合をig/initでまとめて初期化している.

そうすると, ig/init-keyで初期化したそれぞれのオブジェクトを1つのオブジェクトに bindingして管理しているようにもみえる. 管理ということで, suspend, resume, haltはオブジェクトを Command Pattern で扱うようなものとして捉えれば納得がいく.

(アナロジーとして類推しただけで実装を読んではない…後で読む).

💡考察: Integrant Rationale cf. Component #

Clojure Component の代替を意識して, とくにComponentが依存関係をプログラム内(Clojure Source Code)で管理するが, IntegrantはEDNで管理するところがこだわりポイント.

すなわちIntegrantはClojure MapでもEDNでもどちらでも構成定義できるが, 設計動機からいえばEDNつかえよ!ということかな?

💡考察: Component/MountとIntegrantの決定的違いはOOP vs FP #

ComponentやMountを使ったことがないので以下はリンク先からの理解.

Integrant: an alternative to Component and Mount : Clojure

ComponentやMountは状態をグローバルに参照することができるので, 関数の引数としてもらう必要がない.

Integrantは状態がライブラリの中に隠されていて自由に参照できない. そのためその状態に対する操作は関数の引数としてもらって変化した値を返すように書く. またはhandlerの定義として状態とそれに対する操作を1つにbindingする.

Webフレームワークならたくさんのサンプルを見ながら自然とこの初期化で状態と関数をhandlerとしてbindingするパターンに従えばいいものの, webとは関係なく単にintegrantを使おうとしたとき, ベストプラクティスがないので自分の流儀で実装しがち, 本質を考えよう.

初期化時に自前でnamespaceにatomに保存しておく方法はそもそもフレームワークで状態を管理する考えに反するアンチパターン.

Integrantは関数型プログラミング(FP)の考えに近い. 一方Componentの考えはクラスやOOPに近い.

とくにFPでシステムを構築すると冪等性を獲得することができ, これが開発時にとくに役に立つ(cf. Reloaded Workflow). Componentは自分が初期化済みかどうかはComponent自身しかわからない.

💡 integrantで状態を保持したいならdef/alter-var-root #

ref. 💡考察: Component/MountとIntegrantの決定的違いはOOP vs FP

それでもオブジェクト指向的にintegrantをつかうならば def macro / alter-var-rootによるvarの再定義のパターンをつかう.

; https://github.com/dimovich/roll/blob/master/src/clj/roll/sente.clj

(defonce sente-fns)

(defn send-msg [& args]
  (some-> (:chsk-send! sente-fns)
          (apply args)))

(defmethod ig/init-key :roll/sente [_ opts]
  (alter-var-root #'sente-fns (constantly (start-sente opts))))

(defmethod ig/halt-key! :roll/sente [_ {:keys [stop-fn]}]
  (info "stopping roll/sente...")
  (alter-var-root #'sente-fns (constantly nil))
  (when stop-fn
    (stop-fn)))

tools.namespace/refreshはdefonceしても強制的にリセットするのでdef/defonceはあまり関係ないかな?

💡IntegrantとRing Handlerの2つのパターン #

2つのパターンがある.

  • すべてのRingハンドラーをコンポーネントとして扱う.
  • ルーター部分までをコンポーネントにして、Ringハンドラーはただの関数として扱う.

Component/MountとIntegrantの決定的違いはOOP vs FP の議論に似ている.

🔗 References #