clojure.spec入門2
「clojure.spec入門1」ではclojure.specを使った仕様の作り方について説明しました。 今回は実際に関数に仕様を登録しましょう。
なお、この文書ではclojure.spec
名前空間にs
、clojure.spec.test
名前空間にt
という別名を付けています。
また、この文書はClojure 1.9.0-alpha7、test.check 0.9.0の使用を前提としています。
(require '[clojure.spec :as s]) (require '[clojure.spec.test :as t])
次のようなmy-range
関数の仕様を考えます。
(defn my-range [begin end] (vec (range begin (inc end)))) (my-range 10 20) ;;=> [10 11 12 13 14 15 16 17 18 19 20]
関数に仕様を登録するにはs/fdef
マクロを使います。
s/fdef
で登録できる仕様には:args
、:ret
、:fn
の3種類があります。
:args
は関数の引数に対する仕様です。
引数はリストとして述語に渡されます*1。
ここでは例として次のような仕様をclojure.specで書きます。
- 第1引数は1以上10000以下の整数
- 第2引数は1以上10000以下の整数
- 第1引数は第2引数よりも小さい
(s/and (s/cat :begin (s/and integer? #(<= 1 % 10000)) :end (s/and integer? #(<= 1 % 10000))) #(< (:begin %) (:end %)))
:ret
は関数の返り値に対する仕様です。
返り値はそのまま述語に渡されます。
ここでは例として次のような仕様をclojure.specで書きます。
(s/and vector? #(every? integer? %) #(every? pos? %) #(= % (sort %)))
:fn
では関数の引数と返り値との関係に対する仕様です。
引数と返り値は{:args s/conform後の引数 :ret s/conform後の返り値}
というマップとして述語に渡されます。
ここでは例として次のような仕様をclojure.specで書きます。
- 返り値のベクターの要素の個数は第2引数 - 第1引数 + 1
#(= (count (:ret %)) (inc (- (:end (:args %)) (:begin (:args %)))))
これらの仕様を次のようにしてmy-range
に登録します。
(s/fdef my-range :args (s/and (s/cat :begin (s/and integer? #(<= 1 % 10000)) :end (s/and integer? #(<= 1 % 10000))) #(< (:begin %) (:end %))) :ret (s/and vector? #(every? integer? %) #(every? pos? %) #(= % (sort %))) :fn #(= (count (:ret %)) (inc (- (:end (:args %)) (:begin (:args %))))))
関数に登録した仕様はそのままでは何もしません*2。
s/instrument
関数を使うと関数に登録した:args
のチェックが有効になります。
(s/instrument #'my-range) (my-range 1 5) ;;=> [1 2 3 4 5] (my-range -10 10) ;;=> ExceptionInfo Call to #'user/my-range did not conform to spec: ;; In: [0] val: -10 fails at: [:args :begin] predicate: (<= 1 % 10000) ;; :clojure.spec/args (-10 10) ;; clojure.core/ex-info (core.clj:4706)
:args
のチェックを再び無効にするにはs/unstrument
関数を使います。
(s/unstrument #'my-range) (my-range 1 5) ;;=> [1 2 3 4 5] (my-range -10 10) ;;=> [-10 -9 -8 -7 -6 -5 -4 -3 -2 -1 0 1 2 3 4 5 6 7 8 9 10]
:ret
と:fn
についてはtest.checkでのジェネレーティブテストで使います。
少し試してみましょう。
(t/check-fn #'my-range (s/fn-spec #'my-range)) ;;=> {:result true, :num-tests 100, :seed 1466393888201}
これでmy-range
について:args
から自動的に作った100組の引数について:ret
、:fn
を満たしているかどうかのテストが行われました。
引数の生成は:args
を登録するときに使ったinteger?
が対応するジェネレーターを持っていることにより実現しています。
なお、:args
のinteger?
と#(<= 1 % 10000)
とを逆にするとジェネレーティブテストは動きません。
(s/fdef my-range :args (s/and (s/cat :begin (s/and #(<= 1 % 10000) integer?) :end (s/and #(<= 1 % 10000) integer?)) #(< (:begin %) (:end %)))) (t/check-fn #'my-range (s/fn-spec #'my-range)) ;;=> IllegalStateException Unable to construct gen at: [:begin] for: (<= 1 % 10000) clojure.spec/gensub (spec.clj:222)
s/and
はジェネレーターについて、第1引数に対応するジェネレーターを使い、第2引数以降をフィルターとして扱います。
そのため、新たに作った述語関数である#(<= 1 % 10000)
を第1引数にした場合、s/and
は適当なジェネレーターを見付けることができず例外が発生します*3。
ここで、clojure.specの利用方法について"clojure.spec - Rationale and Overview"から引用します。
- Documentation
- Functions specs defined via
fdef
will appear when you calldoc
on the fn name. You can calldescribe
on specs to get descriptions as forms.- (日本語訳)
fdef
で定義された関数の仕様は関数名についてdoc
を呼び出したときに表示されます。 説明をフォームの形で得るために仕様についてdescribe
を呼び出すことができます。- Parsing/destructuring
- You can use
conform
directly in your implementations to get its destructuring/parsing/error-checking.conform
can be used e.g. in macro implementations and at I/O boundaries.- (日本語訳)分配束縛、パース、エラーチェックのために実装で直接
conform
を使うことができます。conform
は例えばマクロの実装や入出力の境界で使うことができます。- During development
- You can selectively instrument functions and namespaces with
instrument
andinstrument-ns
.instrument
swaps out the fn var with a wrapped version of the fn that tests all three of the fn specs.unstrument
returns a fn to its original version. You can generate data for interactive testing withgen/sample
.- (日本語訳)
instrument
とinstrument-ns
を使って関数や名前空間を選んでチェックすることができます。instrument
は関数のvarを関数の仕様の3つ全てをテストするバージョンと交換します(現在のinstrument
で交換される関数は:args
のみをテストします)。unstrument
は関数をオリジナルのバージョンに戻します。 インタラクティブなテストのためにgen/sample
を使ってデータを生成することができます。- For testing
- You can run a suite of spec-generative tests on an entire ns with
run-tests
. You can get a test.check compatible generator for a spec by callinggen
. There are built-in associations between many of theclojure.core
data predicates and corresponding generators, and the composite ops of spec know how to build generators atop those. If you callgen
on a spec and it is unable to construct a generator for some subtree, it will throw an exception that describes where. You can pass generator-returning fns tospec
in order to supply generators for things spec does not know about, and you can pass an override map togen
in order to supply alternative generators for one or more subpaths of a spec.- (日本語訳)
run-tests
を使って仕様に基づくジェネレーティブテストを名前空間全体に対して行うことができます。gen
を呼び出すことで仕様に対するtest.checkと互換性のあるジェネレーターを得ることができます。clojure.core
のデータのための述語関数には対応するジェネレーターとのビルトインの関連があります。そして、仕様の複合演算子はそれらのジェネレーターをどのように作るのかを理解します。 もしある仕様についてgen
を呼び出し、どこかのサブツリーについてジェネレーターを作ることができなければ、それがどこであるのかを説明する例外が投げられます。 対応するジェネレーターを知らない仕様に対しては、ジェネレーターを提供するためにジェネレーターを返す関数をspec
に渡すことができます。そして、仕様の1つ以上のサブパスに対して別のジェネレーターを提供するためにオーバーライドマップをgen
に渡すことができます。- At runtime
- In addition to the destructuring use cases above, you can make calls to
conform
orvalid?
anywhere you want runtime checking, and can make lighter-weight internal-only specs for tests you intend to run in production.- (日本語訳)前述の分配束縛の利用例に加えて、実行時のチェックをしたい場所であればどこでも
conform
やvalid?
を呼び出すようにすることができます。そして、プロダクション環境で実行することを意図したテストのためにより軽量で内部的な仕様を書くことができます。
以上のとおり、簡単にclojure.specの使い方を説明しました。 clojure.specにはこの文書では触れていない重要な機能がまだたくさんありますので、この文書を読んでclojure.specに興味を持った方はぜひ参考資料を読んでください。
Clojure 1.9.0はまだアルファ版であり、clojure.specについてもClojure 1.9.0のリリースまでにまだまだ変わっていく可能性があります。
また、clojure.core
名前空間への適用もまだ始まっていません*4。
しかし、clojure.specがClojureに標準で付いてくる以上、今後はclojure.specで仕様を書くというスタイルが一般的になることはほぼ確実だと思いますので、今のうちから少しずつ試してみるのがよいと思います。
参考資料
clojure.spec入門1
clojure.specはClojureでデータ、関数、マクロなどの仕様を書くためのライブラリーです。 clojure.specで仕様を書くことによる利点はいろいろありますが、まずは深く考えずにclojure.specで遊びましょう。
なお、この文書ではclojure.spec
名前空間にs
という別名を付けています。
また、この文書はClojure 1.9.0-alpha7の使用を前提としています。
(require '[clojure.spec :as s])
clojure.specでは仕様をスペックオブジェクトで表します。
スペックオブジェクトはs/spec
マクロに述語関数*1を渡すことで作れます。
次の例では「整数である」という仕様を表すスペックオブジェクトを作ります。
(s/spec integer?)
ある値が仕様を満たしているかどうかを判断するにはs/conform
関数を使います。
次の例では10
、0.2
がそれぞれ「整数である」という仕様を満たしているかどうかを判断します。
(s/conform (s/spec integer?) 10) ;;=> 10 (s/conform (s/spec integer?) 0.2) ;;=> :clojure.spec/invalid
s/conform
は第1引数の仕様を第2引数の値が満たしていないときに:clojure.spec/invalid
を返します。
第1引数を第2引数が満たしているときにs/conform
が何を返すかは第1引数の種類によります(この例では第2引数をそのまま返しています)。
前述の例は実は次のように書くこともできます。
(s/conform integer? 10) ;;=> 10 (s/conform integer? 0.2) ;;=> :clojure.spec/invalid
これはs/conform
の第1引数がスペックオブジェクトよりも広い概念である「述語(predicates)」を受け入れるようになっているからです。
述語には次のものが含まれます。
clojure.spec
内の関数、マクロの多くはs/conform
と同様に述語を受け入れるようにできています。
そのため、s/spec
を使って明示的にスペックオブジェクトを作ることはあまりありません。
ある値が「整数かつ正の数である」という仕様を満たしているかどうかを判断するには次のように書きます。
(s/conform (s/and integer? pos?) 20) ;;=> 20 (s/conform (s/and integer? pos?) -3) ;;=> :clojure.spec/invalid
s/and
マクロは引数として渡された複数の述語を「かつ」で結合します。
s/and
の返り値はスペックオブジェクトです。
ある値が仕様を満たしていないとき、仕様のどの部分を満たしていないのかを知りたい場合にはs/explain
関数を使います。
(s/explain (s/and integer? pos?) -3) ;;=> val: -3 fails predicate: pos? (s/explain (s/and integer? pos?) 0.1) ;;=> val: 0.1 fails predicate: integer?
なお、前述の例は次のようにも書けます。
(s/conform #(and (integer? %) (pos? %)) 20) ;;=> 20 (s/conform #(and (integer? %) (pos? %)) -3) ;;=> :clojure.spec/invalid
しかし、このように書いてしまうと仕様を満たしていないときに仕様のどの部分を満たしていないのかを知ることが難しくなります。
(s/explain #(and (integer? %) (pos? %)) 0.2) ;;=> val: 0.2 fails predicate: :clojure.spec/unknown
複数の述語を「かつ」で結合するs/and
に対して複数の述語を「又は」で結合するs/or
マクロもあります。
(s/conform (s/or :s string? :b boolean?) true) ;;=> [:b true] (s/conform (s/or :s string? :b boolean?) "Clojure") ;;=> [:s "Clojure"]
s/or
では引数をキーワードと述語とのペアで書きます。
s/or
の返り値はスペックオブジェクトです。
s/or
を使った場合のs/conform
の返り値に注目しましょう。
s/or
を使った場合、s/conform
は分配束縛を行い、その返り値はキーワードと値とのペアのベクターになります。
なお、s/and
は引数として渡された述語を前から順番に検討していきますが、ある述語について仕様を満たすと判断した場合、その述語にs/conform
を適用したときの返り値がそれ以降の述語に渡されます。
つまり、次のようなことが起こります。
(s/conform (s/and (s/or :s string? :b boolean?) string?) "Clojure") ;;=> :clojure.spec/invalid
s/and
の第1引数の述語を検討した後、第2引数の述語に渡されるのは"Clojure"
ではなく[:s "Clojure"]
になります。
(s/conform (s/and (s/or :s string? :b boolean?) vector?) "Clojure") ;;=> [:s "Clojure"]
「第1要素が整数かつ第2要素が小数であるシーケンスである」という仕様を表すにはどうすればよいでしょうか。 まずは次の方法が考えられます。
(s/conform (s/and #(pos? (first %)) #(neg? (second %))) '(1 -2)) ;;=> (1 -2)
しかし、s/cat
マクロを使うともっとよい感じに書くことができます。
s/cat
では引数をキーワードと述語とのペアで書きます。
(s/conform (s/cat :p pos? :n neg?) '(1 -2)) ;;=> {:p 1, :n -2}
s/cat
は渡された引数の述語を連結し、シーケンスにマッチする「正規表現演算子(regex ops)」を作ります。
clojure.specではs/cat
、s/alt
マクロ、s/*
マクロ、s/+
マクロ、s/?
マクロ、s/&
マクロの返り値を正規表現演算子と呼びます。
s/cat
を使った場合、s/conform
は分配束縛を行い、その返り値はキーワードと値とのペアのマップになります。
なお、s/cat
とs/and
を組み合わせると、次のようなこともできます。
(s/conform (s/and (s/cat :i integer? :f float?) #(< (:i %) (:f %))) '(3 6.4)) ;;=> {:i 3, :f 6.4}
このような形で「第1要素が整数かつ第2要素が小数かつ第1要素が第2要素よりも小さいシーケンスである」という仕様を表すことができます。
(「clojure.spec入門2」に続く)
参考資料
*1:この文書では引数を1つ受け取り、ブール値を返す関数を述語関数と呼びます
ClojureScriptのコンパイルパイプライン
この文書について
この文書はMichael Fogusさんの「The ClojureScript Compilation Pipeline」を日本語に翻訳したものです。 原文のライセンスと同じく、この文書はクリエイティブ・コモンズ 表示 - 継承 2.5 一般 ライセンスの下に提供されます。
これは「解説Clojureのコンパイル技術」シリーズの5番目の記事です。
来るべきClojureのGoogle Summer of Codeプロジェクトを記念して、私はClojureScriptのコンパイラーパイプラインについてのいくつかの解説を公開します。 私はこれについて私のClojureWestでのトークで話しました。しかし、もう少しの検討は歓迎です。 ここで私が述べることのほとんどは、データ構造と実際のプログラム上のインターフェイスの周りの詳細が異なることを除いてClojureのパイプラインにも適用されます。 私は抽象度の高いレベルにとどまろうと試みるつもりです。
ClojureScriptのパイプライン概観
これはClojureScriptのパイプラインです。
あなたがClojureコードを片端に入れると、もう一方の端からJavaScriptが得られます。
コンパイラーは非常に多くのフェーズから構成され、その最初のフェーズは文字列の読込みとそれらをClojureのデータ構造に変換することに充てられます。
次のコードをClojure REPLで観察することで、あなたはどのように読込みフェーズが動くのかが分かるでしょう。
(def E (read-string "(-> 42 (- 6) Math/sqrt)")) (type E) ;=> clojure.lang.PersistentList (type (last E)) ;=> clojure.lang.Symbol
あなたはE
がデータ構造であることが分かるでしょう。
次のコンパイルフェーズはマクロ展開フェーズです。
このフェーズの間に、以下のとおり、フォームはそれがある不動点に到達するまで展開されるでしょう。
(-> E macroexpand-1) ;=> (clojure.core/-> (clojure.core/-> 42 (- 6)) Math/sqrt) (-> E macroexpand-1 macroexpand-1) ;=> (Math/sqrt (clojure.core/-> 42 (- 6))) (-> E macroexpand-1 macroexpand-1 macroexpand-1) ;=> (. Math sqrt (clojure.core/-> 42 (- 6))) (-> E macroexpand-1 macroexpand-1 macroexpand-1 macroexpand-1) ;=> (. Math sqrt (clojure.core/-> 42 (- 6)))
上の実例では3つのレベルのマクロ展開でフォームは(あるレベルから次のレベルに変わらない)不動点に到達しました。
結局は内部の->
マクロも展開するでしょうが、それはASTの生成中にフォームが巡回されるときに起きます。
あなたは私がマクロ展開の箱を少し小さく作ったことに気付くでしょう。
この違いの意味はマクロ展開がASTの生成に割り込むということです。
機械的なマクロ展開の他に、このフェーズは.
(ドット)演算子の引数を同じ意味の( . target field/method args)
形式に入れることもします。
次のフェーズは解析フェーズであり、その主目的はClojureScriptの抽象構文木(abstract syntax tree、AST)を生成することだと思われます。
もしあなたが私とASTについて話したことがあれば、不幸なことにあなたはおそらく「Lispの構文はプログラム自体のASTである」という不運な言葉(又はいくつかのそのようなバリエーション)に対する私の先入観の全てを聞いてしまったでしょう。 これはごみくずです。 実際のASTはローカル束縛の情報、アクセス可能な束縛、アリティーの情報、その他多くの便利な小さな情報のような船1杯分の追加情報で装飾されています。
あなたはおそらくこのような図を(そしておそらくパイプラインに似た画像も)他のブログの記事、教科書、論文で見たことがあるでしょう。 しかし、ClojureScript(そしてある日のClojureコンパイラー)の提供する1つの利点は各コンパイルフェーズ間でインターフェイスが単なるClojureのデータで構成されているということです! リーダーの生成物はリスト、いくつかの他のClojureのデータ型、又はそれらのネストしたものです。 マクロ展開の生成物も同じです。 解析フェーズの生成物はネストしたClojureのマップから構成されたAST自体です。 Clojure又はClojureScript自体(そして数百のライブラリー)が直接処理するツールそのものによって処理できないコンパイルフェーズの生成物はありません。 これはClojureとLisp全般の本当にすばらしい機能です。
最後のステージはASTを受け取りJavaScriptを生成するエミッションフェーズです。
エミッションはおそらくClojureScriptコンパイラー全体で最も複雑な部分でしょう。それはコード700行くらいです。
ClojureScriptのパイプラインへの接続
まず明らかな拡張場所は解析フェーズのバックエンドです。
これは実際にAmbrose Bonnaire-Sergeantさんが彼のtyped-clojureで採用したアプローチです。 大きな違いは彼の解析プロジェクトがClojureの解析エンジンを使ってClojureScriptに似たASTを提供していることです。 これは非常にクールです。 図で示せば、あなたは次のように置かれたtyped-clojureを想像できるでしょう。
解析フェーズで生成された木を型チェッカーを使って装飾し、検査します。 ここで質問です。Haskellの型システムの最大の制限は何でしょうか。 それについて考えましょう。 私は私のトークの中ではこれに答えますが、それはこの記事の中心ではありません。
ただし、概念的にはAST上で順番に起きる追加のチェックを想像することができるので、そこで止める理由はありません。
それゆえ、派生モデルではパイプラインを通じてASTが徐々に装飾されます。 これがプラガブル *1 コンパイルシステムです。それはClojureの理想である開かれた拡張に完全に従ったものです。
これは非常に強力なモデルですが、潜在的に複雑さに満ちています。
しかし、強力な一方、シーケンシャルモデルはとても便利なものでも望まれたものでもありません。 むしろ、よりよいモデルは分岐やパイプラインへのロジックの入力を許すものでしょう。 例えば、プログラムが独立していて完全に型付けされていれば、分岐(beta)が適切な動作かもしれません。 しかし、プログラムが型付けされていないライブラリーの使用を含んでいれば、おそらく実行時制約チェックで装飾された部分的な静的型付け *2 (alpha)がより適切な分岐先かもしれません *3 。
解析フェーズへのインターフェイスとその構造を処理するための一連のツールの設計は比較的単純です。 難しい部分はそれらの使い方、パイプラインへの入力、分岐ロジックです。 ここでは注意深い設計が要求されます。
次の拡張場所はエミッションフェーズの入力部分です。
もちろん、拡張によって私はそれら独自のエミッターをプラグインできるものを意図しています。
これはおそらく新しいプラットフォームをClojureScriptのターゲットにするための最も単純な方法であり、実際にclojure-schemeによって採用されています。 つまり、clojure-schemeを使ってNathan SorensonさんはバックエンドにGambit Schemeを生成したということです。 生成されたSchemeはさらにCにコンパイルすること、最後には機械語にコンパイルすることができます。
ただし、このアプローチには関門があります。 ClojureScriptのコンパイラーは非常に単純で、その中のコードの刈込みではほんの少ししか作業をしません。しかし、代わりにその作業をGoogle Closureコンパイラーに委託します。 もしあなたのターゲット言語がその独自の処理(例えばツリーシェイキング)を持たなければ、あなたはあなた独自のものを作る必要があるでしょう。いっそのこと、ClojureScriptをそうするように拡張するという手もあります。
最後の明らかな拡張場所はエミッションフェーズまでのパイプラインを全てあなた独自のパーサーに置き換えることです。
これは非常に大きな作業(言語のパーサーを1から書く)又は適度に大きな作業(既存のパーサーを修正する)のどちらかでしょう。
ClojureScriptのパイプラインは様々な次元に沿って拡張することができますが、まだツールとインターフェイスの周りでいくつかの仕事を必要としています。 Clojureのコンパイラーの拡張性はもう少し低いですが、絶対不可能ということではありません。 パイプラインへの入力という要望と同じ体験を提供するためにはもっと仕事が必要です。 私はclojure-pyによって採用された他のアプローチには言及していません。 つまり、あなたはClojure実行環境全体をあなたの選択したターゲット言語に実装することを選択するかもしれないということです。
あなたが採用を決めたアプローチ(もしあれば)にかかわらず、楽しみ、ロックし続け、何かすばらしいものを作りましょう。
:F
この記事の草稿を読んでくれたことについてCraig Anderaさん、Bobby Calderwoodさん、Rich Hickeyさんに感謝します。
*1:Gilad Brachaさんはプラガブル型システムについてhttp://bracha.org/pluggableTypesPosition.pdfにある彼の論文「プラガブル型システム」で述べています
*2:再びGilad Brachaさんで「型は反モジュラーである」
*3:私はdocsフェーズを切り離された分岐に沿って置きましたが、全ての分岐への入力として提供する方がより適切かもしれません