component入門

背景

アプリケーションの中で一度だけ行いたい初期化・終了処理を書くことはよくあります. 例えば,データベースへの接続や外部リソースの初期化などです.

Clojureプログラムにおいて,このような初期化・終了の処理というのは意外と面倒です. 普通に走るプログラムであれば -main 関数などの開始時・終了時に処理を書けばよいのですが,ClojureプログラムはREPL環境で開発される場合が多いので,そうもいきません.

例えば,データベースへのハンドラを管理する場合を考えてみましょう.アプリケーションの開始時にコネクションを作成し,終了時に開放するとします.一番簡単なのは,varに束縛してしまうことです.

(def conn (create-db-conn))

(defn foo []
  ;; connを使う
  )

しかし,これだとconnの解放のタイミングがよくわかりません.REPLのセッション中ではずっとコネクションを維持したいですから,foo関数の中で開放してしまうわけに行きません.また,コードを更新後にリソース(上の例で言うconn)をREPL上で作り直したいこともあります.これは,手動で connの解放・再作成をすればOKですが面倒です.

さらに,このような初期化・解放処理が必要なリソースがプログラム中にいくつも存在する場合はどうでしょう?さらに,それらのリソース間に依存性があることも考えられます.例えば,データベースコネクションに依存したリソースはよくありそうですね.それらも含めてリフレッシュしたい場合は?あちこちの名前空間に多数のvarが存在するようになると,アプリケーションの開発効率とメンテナンス性は急速に悪化します.

結局のところ問題は,「アプリケーションの名前空間内にmutableな状態が存在する」というところにつきます.Clojureはimmutableなデータ構造を使ってプログラムを書けるのがよいところですが,時には状態を持ったオブジェクトを管理する必要もあるわけです.このような構造をアプリケーションを支援してくれるのが stuartsierra/component です.

概要

componentを使ったプログラミングは,大きくわけて2つの要素からなります.

コンポーネントは,オブジェクト指向でいうオブジェクトに近いものですが,通常のClojureアプリケーションではかなり粗粒度のオブジェクトになるでしょう.典型的には各ネームスペースに1つのコンポーネントを定義することが多いようです.またコンポーネント同士は依存関係を持つことができます.

システムはコンポーネントの集合体として定義され,コンポーネント間の依存関係を考慮した初期化・終了処理を実行してくれます.

使い方/各コンポーネントの構築

まずは,stuartsierra/componentをプロジェクトで利用できるようにしましょう. プロジェクトの依存関係に [com.stuartsierra/component “0.3.0”]を追加しておきます.

ここでは,架空のプロジェクトとして,DB接続を管理するDB component, 初期化・終了処理が必要な外部APIを管理するAPI coponent, そしてそれらを利用するcomponent Aがあるとします.

それぞれのcomponentは,component/Lifecycleプロトコルdefrecordを用いて定義します

;; DB接続を管理するComponent
(defrecord DBComponent [conn host port]
  component/Lifecycle

  (start [this]
    (println "Initializing DB component.")
    ;; DB接続の開始処理
    ;; ここでは仮に,"DB connection"という文字列を代入しておく
    (assoc this :conn (format "<DB connection: %s:%d" host port)))

  (stop [this]
    (println "Stopping DB component.")
    ;; DB接続の終了処理
    ;; 注意: this自身からフィールドをnilに設定して返すが,dissocをしてしまうと
    ;; 戻り値が普通のmapになってしまう
    (assoc this :conn nil)))

メインとなるのはstartstop関数で,これらを用いて初期化を行います. 引数として渡されるthisは,コンポーネントのベースとなるrecordが渡されます.アプリケーションの各コンポーネントは,thisに必要なデータを格納していくことでコンポーネントを構築していきます.

使い方/システムの構築

(defn app-system [config-options]
  (let [{db-host :db-host
         db-port :db-port
         api-host :api-host} config-options]
    (component/system-map
     ;; (B)
     :db (map->DBComponent {:host db-host :port db-port})
     :api (map->APIComponent {:host api-host})
     :A (component/using
         (map->ComponentA config-options)
         {:db  :db
          :api :api}))))

component/system-map関数を用いてシステムを構築します.system-map関数にはmapを渡しますが,これがシステム内でのキーとコンポーネントの関係を示します.各コンポーネントは,defrecord時に定義されるmap->xxx関数を使って構築されているのがわかると思います. component/usingは依存関係を定義する関数で,ここではComponentADBComponentAPIComponentに依存していることを示しています.

使い方/REPLでの活用法

REPLでの実行では,user名前空間にシステムをvarとして定義して使っていく方法がメインとなります.

;; systemを定義
(def system (app-system {:db-host "dbhost" :db-port 9999 :api-host "apihost"}))

(alter-var-root #'system component/start) ; systemを開始
;;Initializing DB component.
;;Starting API component
;;Initializing A: db-conn= <DB connection: dbhost:9999  api= <External API session host=apihost>



(alter-var-root #'system component/stop) ; systemを終了
;; Finalizing A
;; Stopping API component
;; Stopping DB component.

これによって,user/system にシステムが束縛されますので,systemを使ったプログラミングを行うことができます.それぞれのコンポーネントは,定義した当該のキーで取り出すことができます

user> (:db system)
#example-ns.DBComponent{:conn "<DB connection: dbhost:9999", :host "dbhost", :port 9999}

tools.namespaceとの連携によってより便利に使うことができますが,また次の機会に紹介したいと思います.

サンプル全体のソースコードこちらにあります.

名前空間付きキーワードの意味と使い方

Clojureにおいて,キーワードは多用されます.実は,キーワードには名前空間(namespace qualifier)をつけることができます.従来はそれほど重要性は高くありませんでしたが,Clojure 1.9におけるcore.specの導入により,にわかに重要性が高まりました.機能自体は以前から存在していますが,Spec Guideを読んではじめてqualifiedキーワードの登場に面食らった人も多いのではないでしょうか.

このエントリーでは,qualifiedキーワードの記法についてまとめ,次にcore.specにおける利用方法,最後にcore.spec以外での利用方法についてまとめます.

名前空間付きキーワードの記法

Clojureにおいては,一般のvarが名前空間に属するのと同様にキーワードも名前空間に属することができます.名前空間付きキーワードは, :namespace/foo という文法で記述することができます.qualifiedキーワードは,unqualifedキーワードとは違うものとして扱われます.また,違う名前空間に属する場合は異なるキーワードとして扱われます.

user=> :my-ns/foo
:my-ns/foo

user=> (= :foo :my-ns/foo) ;; unqualified / qualified キーワードは互いに等価でない
false
user=> (= ::foo :foo) 
false

user=> (= :my-ns2/foo :my-ns/foo) ;; 名前空間が違う場合は等価でない
false

簡略化の記法として,現在の名前空間に属するキーワードは ::fooのように書くことができます

user=> ::foo
:user/foo

user=> (= ::foo :user/foo)
true

また,Clojure 1.9 alpha-8 から導入された省略記法として,#:記法があります.mapのキーにキーワードが使われている場合に記述を省略できます(alphaリリースなので将来消される可能性もありますが,alpha-10の時点では残っています).

;; 1.9 alpha-10でテスト
user=> #:my-ns { :key "val" }
#:my-ns{:key "val"}

user=> (def my-map #:my-ns{:key "val"} ;; my-mapのキーは :my-ns/key である
#’user/my-map

user=> (:key my-map)
nil

user=> (:my-ns/key my-map)
"val"

core.specにおける利用法

core.specにおいては,core.spec/def を用いてspecをキーワードとひも付けてregistryに登録します.

;; http://clojure.org/guides/spec より引用
(s/def ::date inst?)

ここではキーワードを登録のキーとして使っており,そして(おそらく)registryはグローバルに管理されているので,unqualifiedキーワードを使ってしまうと衝突の危険があります.なので,registryのキーとしてキーワードを指定する際は必ずqualifiedなキーワードを使うべきです.普通は現在の名前空間に登録するでしょうから,「Spec Guide」では一貫して::記法を利用した qualifiedキーワードが使われています.

core.spec以外での使用法

はっきりいって,通常の record の使用の際にqualifiedキーワードを使うことはそれほど多くないでしょう.

有用なのは,ライブラリ同士or名前空間同士で意図しない衝突を避けたい場合です.[3]では,re-frameというclojurescriptフレームワークで,イベントハンドラの名前(=キーワード)が名前空間同士で衝突しないようにqualifiedキーワードを使うことが述べられています.

[2]では,Ring v0.1ではリクエストとレスポンスのmapにqualifiedキーワードが使われていたけれどもv0.2ではunqualifiedに変更されたという経緯についても触れられています.

参考文献

翻訳:"Inside Clojure's Collection Model"

The original article is written by Alex Miller (@puredanger). Translated by Keisuke Fukuda (@keisukefukuda)

この記事は,”Inside Clojure’s Collection Model”の日本語訳です.原著者のAlex Millerさんの許可を得て公開しています.翻訳に関する指摘は翻訳者まで.(athosさんありがとうございました)

Alex Millerさんは,Clojureの主要な開発者の一人であり,リリースマネージャの役割もされ,メーリングリストにおける活動量も非常に多い方です.また,Clojure Applied: From Practice to Practitionerの著者でもあります.

Clojureのコレクションについて,私(翻訳者)の記事「Clojureにおけるデータ構造の抽象化を理解して独自のデータ構造を実装する」も参照してください

Inside Clojure’s Collection Model

Clojureのコレクション型には何があるでしょうか?ほとんどのClojurianは,list, vector, map, setで十分だと言うと思います.seqを加えてもいいかもしれません.Seqは,コレクションの抽象化(View)で,コレクションを使って実装することもできるし,別の何かを使って実現することもできるものです.(この点についてはSequencesを見てください.)

しかし,Clojureにはsorted map, sorted set, queue, 組み込み型の配列,Javaの配列,Javaのコレクションなどなど,そしてもちろん deftype によるユーザー定義の型があります.ここまでくると,だいぶ話が込み入ってくるでしょう.そして,判定関数(sequential?, seq?, coll?)がこれらのコレクション型に対して返す値は,わけがわからないように見えるかもしれません.

少し調べるとわかりますが,コレクションには,一見してわかるよりも,込み入った事情があります.Clojureは,日々使われる基本機能を提供するために,いくつかの異なる層を提供しています.最も優れた点は,Clojureの機能(コンパイラ,ランタイム,標準ライブラリ)はコレクションの実装には全く依存せず,拡張可能な抽象化層の上に 構築されているという点です.Clojure初心者のうちはこれらの拡張機能に触ることはほとんどないでしょうが,より洗練されたプログラムを書くためにこれらの機能が有用だということが徐々にわかってくることでしょう.

Traitと判定関数

Clojureのコレクション・ライブラリの中で最も重要な層は,おそらくTraitでしょう(Traitというのは私独自の用語です).TraitはClojureのProtocolの機能を用いて実装されているのが理想ですが,これらの機能はProtocolの導入よりも前に遡るので,Javaのインターフェースとして定義されています.

重要なtraitクラスをいくつか紹介しましょう(すべてclojure.langパッケージにあります):

  • Counted - 数え上げ可能(加算*1)なコレクション
    • count()
  • Indexed - Countedをextendしていて,インデックスによる参照が可能
    • nth(int i)
    • nth(int i, Object notFound)
  • Sequential
    • シーケンシャルなコレクションのためのマーカーインターフェース
  • Associative (ILookupをextend)
    • containsKey(Object key)
    • entryAt(Object key)
    • assoc(Object key, Object val)
    • ILookup経由:
      • valAt(Object key)
      • valAt(Object key, Object notFound)
  • Sorted - 並べ替え済みコレクションのためのマーカー
  • Seqable*2 - sequenceを生成することができるコレクション
    • seq()
  • Reversible - 逆向きsequenceを生成できるコレクション
    • rseq()

それぞれのインターフェースは,コレクションが持ちうる特性・属性の一部を示しています.そして,ほとんどの判定関数は,それぞれのインターフェースをimplementしているかどうかをチェックしているに過ぎないということがわかります.

  • counted? - Countedをimplementしているか
  • indexed? - Indexedをimplementしているか
  • sequential? - Sequentialをimplementしているか
  • associative? - Associativeをimplementしているか
  • sorted? - Sortedをimplementしているか?
  • reversible? - Reversibleをimplementしているか

上の表で欠けているのはILookupSeqableです.ILookupは,Associativeの非常に小さなサブセットで,Associativeの方が汎用です.Seqableは少し厄介で,文字列や配列をseqableと判定するために,Clojureはちょっといろいろなことをやっています.もし当時Protocolが存在していれば,このような「閉じた型」についてもSeqableが直接的に実装されていたでしょう*3.これについては「Sequences」という記事を読んでください.

重要な事は,clojure.coreclojure.lang.RTにある集合関数のほとんどは,これらのtraitを操作するのであって,具体的なコレクションの型や,コレクションのインターフェースを操作するわけではないということです.

これを追いかけるのは少々大変です.ほとんどの標準のコレクション関数(count,nth, get, assoc, seq)は上記のインターフェースと明示的に対応しています.Clojureのcore関数のソースコードを見ると,それらのメソッドの殆どがRTクラスへ転送されていることがわかります.そしてRT.count()をみると,まず第一の実装として上記のインターフェースの関数が起動されています(そして,いくつかの特殊ケースについては特別な実装がされています).もう一度書きますが,もしProtocolを使えばこれらの実装はより綺麗になるはずです.

この実装の利点は,これらのtraitをいくつか実装した型を作れば,既存のClojureの実装に手を加えることなく,協調して動作することができるということなのです

Collections

Clojureのコレクションも,Javaインターフェースの実装が最初に行われています.大半のインターフェースは,コレクションがどのようなtraitsを持つかどうかを定義するものです. (実は,詳細については少し誤魔化しています.これらの実装が行われている実際のクラス階層は少々ややこしいからです).わかりやすい例は,IPersistentVectorで,これはAssociativeSequentialReversibleIndexedをextendしています.似たようなインターフェースとして,IPersistentSetIPersistentMapIPersistentListと,より上位レベルのIPersistentCollectionがあります.ここで説明はしませんが,それ以外にもclojure.corepeekpopを通して使用できるIPersistentStackがあります

最後に,多くの具体型があります(実際には,型ごとに複数存在しています)

  • IPersistentList
    • PersistentList - 普通のリスト
    • PersistentList$EmptyList - 特別な場合(空リスト)
    • PersistentQueue - キューの実装
  • IPersistentVector
    • PersistentVector - 普通のベクタ
    • MapEntry - 2要素のpersistent vectorとして振る舞うmapの要素
    • SubVector - subvecを通して生成される,元のベクタのview
    • clojure.core/Vec - 組み込み型のベクタの実装
  • IPersistentSet
    • PersistentHashSet - 普通のset
    • PersistentTreeSet - 並べ替え済みset
  • IPersistentMap
    • PersistentArrayMap - 0〜8要素の時に使われる配列ベースのmap
    • PersistentHashMap - 普通のhash map
    • PersistentTreeMap - 並べ替え済みmap
    • PersistentStructMap defstructから生成される(現在は実質的に廃止)
    • defrecordは,すべてIPersistentMapインスタンスを実装する

しかし,シーケンスについてはどうなのでしょう?ここでどのような役割を果たしているのでしょうか?ここから,話が少しややこしくなってくるのです

シーケンス

シーケンスは,論理的なイミュータブルコレクションを表します.シーケンスがコレクションから生成された時は,コレクションの上に構築されたシーケンスということになりますが(私はこれらのシーケンスを「コレクションのView」だと考えています),一方で関数から必要に応じて生成することもできますし,それ以外のデータから生成することもできます(文字列,配列,イテレータなど)

ここで重要なのは,シーケンスもまたJavaのインターフェースによって実装されているということです

  • ISeq - シーケンスの抽象化で,seq?判定関数でチェックできる
  • Seqable - (前述)シーケンスを生成することができるもの

ここでややこしいのが,ISeqIPersistentCollectionを実装しているということです.よって,シーケンスは実質的にコレクションとして扱うこともできますが,IPersistentListではありません.紛らわしいのは,シーケンスとリストはどちらも括弧を用いて表示されるということです.

コレクションの時の同じように,シーケンスの実装はたくさんあります

  • LazySeq 遅延シーケンス
  • Cons cons呼び出しの結果
  • ChunkedCons mapfilterなどのチャンクされた遅延シーケンスの結果
  • ArraySeq - 配列上のシーケンス
  • StringSeq - 文字列上のシーケンス
  • 他にもたくさん!
等価性

Clojureのコレクションは,等価性における種別(Equalily Partition*4)(sequential, map, set)に基づいて定義されています.等価性は,常に値の比較によっています:コレクション同士が等価性における同じ種別かどうか,そして同じ値を含むかどうか?ということです.

ここが,Clojureが他の言語と大きく異なる点です.Clojureのコレクションは 1) immutableであり 2) 値によって等価性が判定されます.この結果,Clojureのコレクションを値の合成として扱うことができ,イミュータブルで単純な値を使うことの利点を享受できるのです.

シーケンシャルなコレクション(list, vector, シーケンス,そしてsequentialであるとマークされたものであれば何でも)では,比較は先頭から末尾へ要素ごとに行われます.シーケンシャルなコレクションは,比較の際にコレクションの種類をチェックしません.

これは,場合によっては欲しい挙動ではないでしょう(例えば戻り値のテストをするときなど).しかし,比較においてシーケンシャルなコレクションを同一視することは,Clojureにおける利便性のための実用的で優れた選択で,特にシーケンスとコレクションの境界線を曖昧にすることに効果を発揮します.

Setは,完全に同一の集合で,互いに余計な要素を持たない時に等しいと判定されます(ここで「同一」といっているのは 「= 演算子trueを返す」という意味で,オブジェクトの同一性を指しているわけではありません).

Mapは,互いに同一のキーを持っていて,それぞれのキーが互いに同一の要素を返すときに等しいと判定されます.

まとめ

Clojureのコレクションが水面下でどのように実装され,抽象化を用いてどのように統合されているかということの概要を理解するのに,この記事が役立てば幸いです.これらの抽象化は,追加のコンポーネントを組み込むために使うことができますので,これは次の記事のテーマで扱うかもしれません.

2016年3月16日

Originally post written by Alex Miller Translated by Keisuke Fukuda

*1:訳注:Countableを直訳すると「加算」となるかもしれませんが,これだと数学の集合論の加算と紛らわしいので迷いました.

*2:訳注:「しーかぶる」と発音するらしいです

*3:訳注:Javaのインターフェースは,既存の型に後からインターフェースを追加で実装することはできません.これが,「閉じた」型と呼ばれています.一方,ClojureのProtocolは,既存の型(Javaの型であっても)を後から拡張することができます.これが「開かれた/拡張可能な」型ということです

*4:訳注:Equality Partitionの適切な邦訳はわかりません.どなたかご教授ください

記事紹介:Clojureプログラムを,Shebangで直接実行可能にするInlein

Clojure記事紹介<百日修行>7日目.)

hypirion.com

Clojureでプログラムを書く際の面倒な点として,

  • Leiningenによってプロジェクトを生成しなければいけないこと(そして結構時間が掛かること)
  • Clojure処理系の起動が遅いこと

があります.前者については,プロジェクトごとに依存性が管理されることはメリットが多く,一概に問題とはいえませんが,軽いスクリプティングには向きません. 後者についてはいろいろと対策はあるものの,新しいプログラムをインストールしなければいけなかったりしてハードルが高いです.また, サーバーサイドの用途で使う場合は問題になりにくく,言語デザインとして起動時間より柔軟性・機能を取るという選択がされているので,将来的に 大幅に改善されるかどうかも不明です.

このInleinというプロジェクトは,「Clojureスクリプティングをするため」という単一の目的で開発されていて,lein-execのような 別のツールのおまけ的な扱いではありません.スクリプトファイルごとにClojureのバージョン&JVMのオプションを指定するような柔軟な使い方はできません.

ちなみに,ClojureScriptでも良いという方には,Planck のようなツールで 似たようなことができます.ClojureScriptはJavascriptベースなので,既存のV8などのJavascriptエンジンを使うため,起動が非常に高速です.

Inlein,まだ使ったことがないのですが,かなり使ってみたいツールです.使ってみたらレポートを上げたいと思います. たぶん,「インライン」と発音するんでしょうね

↓宣伝.買ったのですがまだ全部読めていません.読み終わったら感想書きます

Clojure Applied: From Practice to Practitioner

Clojure Applied: From Practice to Practitioner