(spec/keys :req [::x ::y (or ::secret (and ::user ::pwd))] :opt [::z])
Clojureは動的言語です。とりわけ、これはコードを実行するために型アノテーションが必須ではないことを意味します。Clojureには型ヒントのサポートがありますが、それは強制メカニズムではなく、包括的でもなく、効率的なコード生成を支援するためにコンパイラーに情報を伝達することに限定されています。Clojureは、JVM自体によって、より豊富な型のランタイムチェックを行います。
しかし、Clojureの指導原理であり、コミュニティによって広く尊重され、実践されてきたのは、情報を単純にデータとして表現することでした。したがって、Clojureシステムの重要なプロパティは、ランタイム型が区別できない異種マップやベクターであるため、データの形状やその他の述語的プロパティによって表現および伝達されますが、どこでもキャプチャまたはチェックされません。
構造を仕様化するためのほとんどのシステムは、キーセット(例えば、マップ内のキー、オブジェクト内のフィールド)の仕様を、それらのキーによって指定された値の仕様と混同しています。つまり、そのようなアプローチでは、マップのスキーマは、:a-keyの型はx-typeで、:b-keyの型はy-typeであると言うかもしれません。これは、硬直性と冗長性の主な原因です。
Clojureでは、マップを動的に構成、マージ、構築することで力を得ています。オプションおよび部分的なデータ、信頼性の低い外部ソースによって生成されたデータ、動的なクエリなどを日常的に扱います。これらのマップは、同じキーのさまざまなセット、サブセット、インターセクション、および和集合を表しており、一般的に、使用される場所に関係なく、同じキーには同じセマンティクスが必要です。すべてのサブセット/和集合/インターセクションの仕様を定義し、各キーのセマンティクスを冗長に記述することは、アンチパターンであり、最も動的なケースでは機能しません。
多くのユーザー、特に初心者は、手書きの解析コードとデストラクチャリングコードによって生成されるエラーメッセージ、特にマクロでは実行の2つのコンテキスト(マクロはコンパイル時に実行され、その展開はランタイム時に実行され、どちらもユーザーエラーが原因で失敗する可能性があります)によって、不満を感じ、困難に直面しています。これにより、「マクロ文法」の要求が生じましたが、実際にはマクロは単なるdata→dataの関数であり、データ検証とデストラクチャリングのソリューションは、他の関数と同様にそれらにも機能するはずです。つまり、マクロは上記の問題のインスタンスです。
最後に、動的かどうかにかかわらず、すべての言語で、テストは品質にとって不可欠です。多くの重要なプロパティは、一般的な型システムではキャプチャされません。しかし、手動テストは、効果/労力の比率が非常に低いものです。Clojureでtest.checkに実装されているように、プロパティベースのジェネレーティブテストは、手動で作成されたテストよりもはるかに強力であることが証明されています。
しかし、プロパティベースのテストでは、プロパティの定義が必要であり、プロパティの作成には追加の労力と専門知識が必要であり、関数レベルでは、関数仕様と実質的に重複しています。関数レベルの多くの興味深いプロパティは、構造的+述語的な仕様によってすでにキャプチャされているはずです。理想的には、仕様はジェネレーティブテストと統合し、「無料」で特定のカテゴリのジェネレーティブテストを提供する必要があります。
Species - 外観、形、種類、種類、spec(ere)に相当し、見る、みなす Specify - species + -ficus -fic(作る) |
仕様は、何かがどのように「見える」かについてのものですが、最も重要なのは、見られるものであるということです。仕様は、読みやすく、プログラマーがすでに使用している「単語」(述語関数)で構成され、ドキュメントに統合されている必要があります。
人々に関数を別の方法で定義させることを要求しないでください。doc
とmacroexpand
にわずかな変更を加えることで、再定義することなく、独立して書かれた仕様がfn/マクロの動作を飾ることができます。
私たちは間違いを犯すことができない世界に住んでいません(そして住むこともできません)。代わりに、定期的に間違いを犯していないことを確認します。Amazonは、UPS<Trucks<Boxes<TV>>>
を使用してテレビを送信することはありません。そのため、時々電子レンジを入手する可能性がありますが、サプライチェーンは正しさの証明で負担をかけられていません。代わりに、エッジで確認し、テストを実行します。
仕様を証明できるものに制限する理由はありませんが、それが主に型システムが行うことです。システムについて伝達し、検証したいことはたくさんあります。これは、構造的/表現的な型やタグ付けを超えて、例えば、ドメインを絞り込んだり、入力間または入力と出力の関係を詳しく述べたりする述語にまで及びます。さらに、私たちが最も気にするプロパティは、多くの場合、静的な概念ではなく、ランタイムの値のプロパティです。したがって、specは型システムではありません。
すべてのプログラムは、型システムがない場合でも名前を使用し、重要なセマンティクスをキャプチャします。Int x Int x Int
は十分ではありません(長さ/幅/高さまたは高さ/幅/奥行きですか?)。したがって、specには、ラベルのないシーケンスコンポーネントやタグのない共用体バインディングはありません。このユーティリティは、specがユーザーに仕様について伝える必要がある場合(例えば、エラーレポートの場合)、また、ユーザーが仕様のジェネレーターをオーバーライドしたい場合(逆も同様)に明らかになります。すべてのブランチに名前が付けられている場合、パスを使用して仕様の一部について話すことができます。
Clojureは名前空間付きのキーワードとシンボルをサポートしています。ここで、Clojureの名前空間オブジェクトではなく、名前空間修飾名についてのみ話していることに注意してください。これらは悲劇的に十分に活用されておらず、辞書/データベース/マップ/セットで競合することなく常に共存できるため、重要な利点をもたらします。specでは、名前空間修飾されたキーワードとシンボル(のみ)を使用して仕様に名前を付けることができます。情報マップに名前空間付きのキーを使用している人々(私たちが成長を望む実践)は、それらの名前で直接それらの属性の仕様を登録できます。これにより、特に動的なコンテキストで、マップの自己記述がカテゴリー的に変更され、構成と一貫性が促進されます。
var、メタデータなどには何も添付されません。すべての関数には名前空間付きの名前があり、他の場所に保存されている関連データ(例:仕様)へのキーとして使用できます。
Lisps(そしてClojure)では、コードはデータです。しかし、データは、その周囲に言語を定義するまでコードにはなりません。この分野の多くのDSLは、スキーマのデータ表現を目指しています。しかし、述語的な仕様にはオープンで大きな語彙があり、有用な述語のほとんどはすでに存在し、コアや他の名前空間の関数としてよく知られており、または単純な式として記述できます。これらの述語をすべて「データ化」し、場合によっては名前を変更することは、価値がほとんどなく、正確なセマンティクスを理解する上で明確なコストがかかります。代わりに、specは、元の述語と式がそもそもデータであるという事実を活用し、ドキュメントやエラー報告でユーザーと通信するためにそのデータをキャプチャします。はい、これはclojure.spec
の表面領域の多くがマクロになることを意味しますが、仕様は圧倒的に人が作成し、構成される場合は手動で行われます。
上記のように、キーの値の詳細を定義するマップは、サポートされない懸念事項の根本的な複雑化です。マップ仕様は、必須/オプションのキー(つまり、集合のメンバーシップに関するもの)を詳細に記述し、キーワード/属性/値のセマンティクスは独立しています。マップのチェックは2段階で行われ、まず必須キーの存在を確認し、次にキー/値の適合性を確認します。後者は、実行時に存在する(名前空間修飾された)キーがマップ仕様にない場合でも実行できます。これは、構成と動的性のために非常に重要です。
必然的に、人々は実装上の決定を詳細に記述するために仕様システムを使用しようとしますが、それは彼らの損失になります。最も良く、最も役立つ仕様(およびインターフェース)は、純粋に情報的な側面に向けられています。情報仕様のみが、ワイヤーを介して、およびシステム全体で機能します。私たちは常に情報アプローチを優先し、矛盾がある場合はそれを優先します。
基本的な考え方は、仕様は述語の論理的な組み合わせにすぎないということです。基本的には、int?
やsymbol?
のような、使い慣れた単純なブール述語や、#(< 42 % 66)
のように自分で作成した式について説明しています。specは、spec/and
やspec/or
のような論理演算を追加し、仕様を論理的に結合し、詳細なレポート、生成、適合サポートを提供し、spec/or
の場合はタグ付きの戻り値を提供します。
マップキーセットの仕様は、必須およびオプションのキーセットの仕様を提供します。マップの仕様は、キー名のベクターにマッピングする:req
および:opt
キーワード引数を指定してkeys
を呼び出すことによって生成されます。
:req
キーは、論理演算子and
とor
をサポートします。
(spec/keys :req [::x ::y (or ::secret (and ::user ::pwd))] :opt [::z])
specと他のシステムとの最も目に見える違いの1つは、::x
が取りうる値を指定するためのマップ仕様の場所がないことです。:my.ns/k
のような名前空間付きキーワードに関連付けられた値の仕様は、そのキーワード自体で登録し、そのキーワードが表示されるすべてのマップに適用する必要があるというのが、specの(強制された)見解です。これには多くの利点があります。
すべての用途でセマンティクスを共有する必要があるアプリケーションでのそのキーワードのすべての用途の一貫性が確保されます
同様に、ライブラリとそのコンシューマー間の一貫性が確保されます
そうしないと、多くのマップ仕様がkについて一致する宣言を行う必要があるため、冗長性が減少します
名前空間付きキーワード仕様は、マップ仕様でそれらのキーが宣言されていない場合でもチェックできます
この最後の点は、マップを動的に構築、構成、または生成する場合に非常に重要です。すべてのマップのサブセット/ユニオン/インターセクションの仕様を作成することは非現実的です。また、不正なデータのフェイルファスト検出(消費時ではなく導入時)も容易になります。
もちろん、多くの既存のマップベースのインターフェースは名前空間のないキーを使用します。それらを適切に名前空間化された再利用可能な仕様に接続するために、keys
は:req
と:opt
の-un
バリアントをサポートします
(spec/keys :req-un [:my.ns/a :my.ns/b])
これは、非修飾キー:a
と:b
を必須とするマップを仕様化しますが、それぞれ:my.ns/a
と:my.ns/b
という名前の仕様(定義されている場合)を使用して検証および生成します。これにより、名前空間付きキーワードが持つものと同じ能力を非修飾キーワードに伝えることはできないことに注意してください。結果のマップは自己記述的ではありません。
シーケンス/ベクトルの仕様は、標準の正規表現の意味論を備えた、一連の標準の正規表現演算子を使用します
cat
- 述語/パターンの連結
alt
- 述語/パターンのセットの中から1つを選択
*
- 述語/パターンの0回以上の出現
+
- 1回以上
?
- 1回またはなし
&
- 正規表現演算子を受け取り、1つ以上の述語でさらに制約します
これらは任意にネストして、複雑な式を形成します。
cat
とalt
は、すべてのコンポーネントにラベルが付けられている必要があり、それぞれの戻り値は、一致したコンポーネントに対応するキーを持つマップであることに注意してください。このようにして、specの正規表現は、デストラクチャリングおよび解析ツールとして機能します。
user=> (require '[clojure.spec.alpha :as s])
(s/def ::even? (s/and integer? even?))
(s/def ::odd? (s/and integer? odd?))
(s/def ::a integer?)
(s/def ::b integer?)
(s/def ::c integer?)
(def s (s/cat :forty-two #{42}
:odds (s/+ ::odd?)
:m (s/keys :req-un [::a ::b ::c])
:oes (s/* (s/cat :o ::odd? :e ::even?))
:ex (s/alt :odd ::odd? :even ::even?)))
user=> (s/conform s [42 11 13 15 {:a 1 :b 2 :c 3} 1 2 3 42 43 44 11])
{:forty-two 42,
:odds [11 13 15],
:m {:a 1, :b 2, :c 3},
:oes [{:o 1, :e 2} {:o 3, :e 42} {:o 43, :e 44}],
:ex {:odd 11}}
仕様を定義するための主な操作は、s/def、s/and、s/or、s/keys、および正規表現演算子です。述語関数または式、セット、または正規表現演算子を受け取ることができるspec
関数があり、述語によって暗示されるジェネレータをオーバーライドするオプションのジェネレータも受け取ることができます。
ただし、def、and、or、keys
のspec関数と正規表現演算子はすべて、述語関数とセットを直接使用でき、spec
でラップする必要はありません。spec
は、ジェネレータをオーバーライドするか、ネストされた正規表現が同じパターンに含まれるのではなく、新たに開始することを指定する場合にのみ必要になります。
仕様を名前で再利用できるようにするには、def
を介して登録する必要があります。def
は、名前空間修飾されたキーワード/シンボルと、仕様/述語式を受け取ります。慣例により、データ仕様はキーワードの下に登録する必要があり、属性値は属性名キーワードの下に登録する必要があります。登録すると、spec操作のいずれかで仕様/述語が呼び出される場所ならどこでも名前を使用できます。
関数は、3つの仕様(引数用、戻り値用、および引数と戻り値を関連付ける関数の操作用)を介して完全に仕様化できます。
関数の引数仕様は常に、引数をリストであるかのように仕様化する正規表現になります。つまり、関数をapply
に渡すリストです。このようにして、単一の仕様で複数のアリティを持つ関数を処理できます。
戻り値の仕様は、単一の任意の値の仕様です。
(オプションの)関数仕様は、引数と戻り値の関係(つまり、関数の関数)に関するさらなる仕様です。これは、{:args conformed-args :ret conformed-ret}
を含むマップ(例えば、テスト中)に渡され、通常はこれらの値を関連付ける述語が含まれます。たとえば、入力マップのすべてのキーが返されたマップに存在することを保証できます。
fdef
の単一の呼び出しですべての3つの関数の仕様を完全に指定でき、fn-specs
を介して仕様を呼び出すことができます。
instrument
で関数と名前空間を選択的にインストルメントできます。これにより、:args
仕様をテストする関数のラップされたバージョンに関数varが置き換えられます。unstrument
は、関数を元のバージョンに戻します。gen/sample
を使用して、インタラクティブテスト用のデータを生成できます。
check
を使用して、名前空間全体に対してスペック生成テストのスイートを実行できます。gen
を呼び出すことで、スペックに対応する test.check 互換のジェネレーターを取得できます。clojure.core
の多くのデータ述語と対応するジェネレーターの間には組み込みの関連付けがあり、spec の複合操作はそれらの上にジェネレーターを構築する方法を知っています。スペックに対して gen
を呼び出し、一部のサブツリーに対してジェネレーターを構築できない場合、それがどこにあるかを説明する例外がスローされます。スペックが知らないものに対してジェネレーターを提供するために、ジェネレーターを返す関数を spec
に渡すことができます。また、スペックの1つまたは複数のサブパスに対して代替ジェネレーターを提供するために、オーバーライドマップを gen
に渡すことができます。
上記のようなデストラクチャリングの使用例に加えて、ランタイムチェックが必要な場所で conform
または valid?
を呼び出すことができ、本番環境で実行するテスト用の軽量な内部専用スペックを作成できます。
より多くの例と使用方法については、spec ガイド および API ドキュメント を参照してください。
spec API の多くの部分で、「述語」または「preds」が要求されます。これらの引数は以下によって満たすことができます。
述語(ブール値)関数
セット
登録されたスペックの名前
スペック (spec
, and
, or
, keys
の戻り値)
正規表現操作 (cat
, alt
, *
, +
, ?
, &
の戻り値)
正規表現内に独立した正規表現述語をネストしたい場合は、spec
の呼び出しでラップする必要があることに注意してください。そうしないと、ネストされたパターンと見なされます。
conform
はスペックを使用するための基本的な操作であり、検証と適合/デストラクチャリングの両方を行います。適合は「深い」ものであり、スペックと正規表現操作、マップスペックなどをすべて通過することに注意してください。nil
と false
は正当な適合値であるため、値を適合させることができない場合、conform は区別された :clojure.spec.alpha/invalid
を返します。valid?
は完全なブール値述語として代わりに使用できます。
スペックについて斬新なものはほとんどありません。上記のすべてのライブラリ、RDF、および Racket のコントラクト など、さまざまなコントラクトシステムで行われたすべての作業を参照してください。
スペックが便利で強力であることを願っています。
Rich Hickey