投票機能付きリンク投稿サイト?clojure.news
Clojure News というサイトが登場したようです.ぱっと見た感じはReddit風のインターフェースですが,投票機能付きのリンク投稿サイトのようですね.楽しみです.
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)))
メインとなるのはstart
とstop
関数で,これらを用いて初期化を行います.
引数として渡される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
は依存関係を定義する関数で,ここではComponentA
がDBComponent
とAPIComponent
に依存していることを示しています.
使い方/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しているか
上の表で欠けているのはILookup
とSeqable
です.ILookup
は,Associative
の非常に小さなサブセットで,Associative
の方が汎用です.Seqable
は少し厄介で,文字列や配列をseqableと判定するために,Clojureはちょっといろいろなことをやっています.もし当時Protocolが存在していれば,このような「閉じた型」についてもSeqableが直接的に実装されていたでしょう*3.これについては「Sequences」という記事を読んでください.
重要な事は,clojure.core
やclojure.lang.RT
にある集合関数のほとんどは,これらのtraitを操作するのであって,具体的なコレクションの型や,コレクションのインターフェースを操作するわけではないということです.
これを追いかけるのは少々大変です.ほとんどの標準のコレクション関数(count
,nth
, get
, assoc
, seq
)は上記のインターフェースと明示的に対応しています.Clojureのcore関数のソースコードを見ると,それらのメソッドの殆どがRT
クラスへ転送されていることがわかります.そしてRT.count()
をみると,まず第一の実装として上記のインターフェースの関数が起動されています(そして,いくつかの特殊ケースについては特別な実装がされています).もう一度書きますが,もしProtocolを使えばこれらの実装はより綺麗になるはずです.
この実装の利点は,これらのtraitをいくつか実装した型を作れば,既存のClojureの実装に手を加えることなく,協調して動作することができるということなのです
Collections
Clojureのコレクションも,Javaインターフェースの実装が最初に行われています.大半のインターフェースは,コレクションがどのようなtraitsを持つかどうかを定義するものです.
(実は,詳細については少し誤魔化しています.これらの実装が行われている実際のクラス階層は少々ややこしいからです).わかりやすい例は,IPersistentVector
で,これはAssociative
,Sequential
,Reversible
,Indexed
をextendしています.似たようなインターフェースとして,IPersistentSet
,IPersistentMap
,IPersistentList
と,より上位レベルのIPersistentCollection
があります.ここで説明はしませんが,それ以外にもclojure.core
のpeek
とpop
を通して使用できるIPersistentStack
があります
最後に,多くの具体型があります(実際には,型ごとに複数存在しています)
IPersistentList
PersistentList
- 普通のリストPersistentList$EmptyList
- 特別な場合(空リスト)-
PersistentQueue
- キューの実装
IPersistentVector
IPersistentSet
PersistentHashSet
- 普通のsetPersistentTreeSet
- 並べ替え済みset
IPersistentMap
PersistentArrayMap
- 0〜8要素の時に使われる配列ベースのmapPersistentHashMap
- 普通のhash mapPersistentTreeMap
- 並べ替え済みmapPersistentStructMap
defstruct
から生成される(現在は実質的に廃止)defrecord
は,すべてIPersistentMap
のインスタンスを実装する
しかし,シーケンスについてはどうなのでしょう?ここでどのような役割を果たしているのでしょうか?ここから,話が少しややこしくなってくるのです
シーケンス
シーケンスは,論理的なイミュータブルコレクションを表します.シーケンスがコレクションから生成された時は,コレクションの上に構築されたシーケンスということになりますが(私はこれらのシーケンスを「コレクションのView」だと考えています),一方で関数から必要に応じて生成することもできますし,それ以外のデータから生成することもできます(文字列,配列,イテレータなど)
ここで重要なのは,シーケンスもまたJavaのインターフェースによって実装されているということです
ISeq
- シーケンスの抽象化で,seq?
判定関数でチェックできるSeqable
- (前述)シーケンスを生成することができるもの
ここでややこしいのが,ISeq
はIPersistentCollection
を実装しているということです.よって,シーケンスは実質的にコレクションとして扱うこともできますが,IPersistentList
ではありません.紛らわしいのは,シーケンスとリストはどちらも括弧を用いて表示されるということです.
コレクションの時の同じように,シーケンスの実装はたくさんあります
LazySeq
遅延シーケンスCons
cons
呼び出しの結果ChunkedCons
map
やfilter
などのチャンクされた遅延シーケンスの結果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
記事紹介:Clojureプログラムを,Shebangで直接実行可能にするInlein
(Clojure記事紹介<百日修行>7日目.)
Clojureでプログラムを書く際の面倒な点として,
- Leiningenによってプロジェクトを生成しなければいけないこと(そして結構時間が掛かること)
- Clojure処理系の起動が遅いこと
があります.前者については,プロジェクトごとに依存性が管理されることはメリットが多く,一概に問題とはいえませんが,軽いスクリプティングには向きません. 後者についてはいろいろと対策はあるものの,新しいプログラムをインストールしなければいけなかったりしてハードルが高いです.また, サーバーサイドの用途で使う場合は問題になりにくく,言語デザインとして起動時間より柔軟性・機能を取るという選択がされているので,将来的に 大幅に改善されるかどうかも不明です.
このInleinというプロジェクトは,「Clojureでスクリプティングをするため」という単一の目的で開発されていて,lein-execのような 別のツールのおまけ的な扱いではありません.スクリプトファイルごとにClojureのバージョン&JVMのオプションを指定するような柔軟な使い方はできません.
ちなみに,ClojureScriptでも良いという方には,Planck のようなツールで 似たようなことができます.ClojureScriptはJavascriptベースなので,既存のV8などのJavascriptエンジンを使うため,起動が非常に高速です.
Inlein,まだ使ったことがないのですが,かなり使ってみたいツールです.使ってみたらレポートを上げたいと思います. たぶん,「インライン」と発音するんでしょうね
↓宣伝.買ったのですがまだ全部読めていません.読み終わったら感想書きます
Clojure Applied: From Practice to Practitioner
- 作者: Ben Vandgrift,Alex Miller
- 出版社/メーカー: Pragmatic Bookshelf
- 発売日: 2015/10/07
- メディア: Kindle版
- この商品を含むブログを見る