Clojure

clojure.spec - 根拠と概要

問題点

ドキュメントだけでは不十分

Clojureは動的言語です。とりわけ、これはコードを実行するために型アノテーションが必須ではないことを意味します。Clojureには型ヒントのサポートがありますが、それは強制メカニズムではなく、包括的でもなく、効率的なコード生成を支援するためにコンパイラーに情報を伝達することに限定されています。Clojureは、JVM自体によって、より豊富な型のランタイムチェックを行います。

しかし、Clojureの指導原理であり、コミュニティによって広く尊重され、実践されてきたのは、情報を単純にデータとして表現することでした。したがって、Clojureシステムの重要なプロパティは、ランタイム型が区別できない異種マップやベクターであるため、データの形状やその他の述語的プロパティによって表現および伝達されますが、どこでもキャプチャまたはチェックされません。

ドキュメント文字列は、人間の利用者に伝えるために使用できますが、プログラムやテストで活用することはできません。つまり、その力は最小限です。ユーザーは、より強力な仕様を取得するために、SchemaHerbertなどのさまざまなライブラリに頼ってきました。

マップの仕様はキーセットのみであるべき

構造を仕様化するためのほとんどのシステムは、キーセット(例えば、マップ内のキー、オブジェクト内のフィールド)の仕様を、それらのキーによって指定された値の仕様と混同しています。つまり、そのようなアプローチでは、マップのスキーマは、:a-keyの型はx-typeで、:b-keyの型はy-typeであると言うかもしれません。これは、硬直性と冗長性の主な原因です。

Clojureでは、マップを動的に構成、マージ、構築することで力を得ています。オプションおよび部分的なデータ、信頼性の低い外部ソースによって生成されたデータ、動的なクエリなどを日常的に扱います。これらのマップは、同じキーのさまざまなセット、サブセット、インターセクション、および和集合を表しており、一般的に、使用される場所に関係なく、同じキーには同じセマンティクスが必要です。すべてのサブセット/和集合/インターセクションの仕様を定義し、各キーのセマンティクスを冗長に記述することは、アンチパターンであり、最も動的なケースでは機能しません。

手動でのパースとエラー報告は不十分

多くのユーザー、特に初心者は、手書きの解析コードとデストラクチャリングコードによって生成されるエラーメッセージ、特にマクロでは実行の2つのコンテキスト(マクロはコンパイル時に実行され、その展開はランタイム時に実行され、どちらもユーザーエラーが原因で失敗する可能性があります)によって、不満を感じ、困難に直面しています。これにより、「マクロ文法」の要求が生じましたが、実際にはマクロは単なるdata→dataの関数であり、データ検証とデストラクチャリングのソリューションは、他の関数と同様にそれらにも機能するはずです。つまり、マクロは上記の問題のインスタンスです。

ジェネレーティブテストとロバスト性

最後に、動的かどうかにかかわらず、すべての言語で、テストは品質にとって不可欠です。多くの重要なプロパティは、一般的な型システムではキャプチャされません。しかし、手動テストは、効果/労力の比率が非常に低いものです。Clojureでtest.checkに実装されているように、プロパティベースのジェネレーティブテストは、手動で作成されたテストよりもはるかに強力であることが証明されています。

しかし、プロパティベースのテストでは、プロパティの定義が必要であり、プロパティの作成には追加の労力と専門知識が必要であり、関数レベルでは、関数仕様と実質的に重複しています。関数レベルの多くの興味深いプロパティは、構造的+述語的な仕様によってすでにキャプチャされているはずです。理想的には、仕様はジェネレーティブテストと統合し、「無料」で特定のカテゴリのジェネレーティブテストを提供する必要があります。

標準的なアプローチが必要

つまり、Clojureには、仕様とテストのための標準的で表現力があり、強力で統合されたシステムがありません。

clojure.specはそれを提供することを目指しています。

目標

コミュニケーション

Species - 外観、形、種類、種類、spec(ere)に相当し、見る、みなす
               + -iēs 抽象名詞接尾辞

Specify - species + -ficus -fic(作る)

仕様は、何かがどのように「見える」かについてのものですが、最も重要なのは、見られるものであるということです。仕様は、読みやすく、プログラマーがすでに使用している「単語」(述語関数)で構成され、ドキュメントに統合されている必要があります。

様々な文脈における仕様の統一

データ構造、属性値、および関数の仕様はすべて同じである必要があり、グローバルに名前空間が設定されたディレクトリに存在する必要があります。

仕様化の取り組みから最大限の活用

仕様を作成すると、自動で以下が可能になるはずです。

  • 検証

  • エラー報告

  • デストラクチャリング

  • 計測

  • テストデータ生成

  • ジェネレーティブテスト生成

侵入を最小限に抑える

人々に関数を別の方法で定義させることを要求しないでください。docmacroexpandにわずかな変更を加えることで、再定義することなく、独立して書かれた仕様がfn/マクロの動作を飾ることができます。

マップ/キー/値を分離する

マップ(キーセット)の仕様を、属性(キー→値)の仕様とは別に保持します。名前空間付きのキーワードから値の仕様への属性粒度の仕様を奨励およびサポートします。キーをセット(マップを指定するため)に結合することは直交になり、チェックは完全に動的なケースでも可能になります。つまり、マップ仕様が存在しない場合でも、属性(キーと値)をチェックできます。

セマンティックな変更と互換性に関する対話を可能にし、開始する

プログラマーは、名前を同じに保ちながら定義し直すと、大きな苦痛を味わいます。しかし、いくつかの変更には互換性があり、いくつかの変更は破壊的であり、ほとんどのツールは区別できません。互換性を判断できる集合メンバーシップや正規表現などの構成を使用し、(一般的な述語の等価性を範囲外に残しながら)互換性チェックのためのツールを提供します。

ガイドライン

間違いは起こりうる

私たちは間違いを犯すことができない世界に住んでいません(そして住むこともできません)。代わりに、定期的に間違いを犯していないことを確認します。Amazonは、UPS<Trucks<Boxes<TV>>>を使用してテレビを送信することはありません。そのため、時々電子レンジを入手する可能性がありますが、サプライチェーンは正しさの証明で負担をかけられていません。代わりに、エッジで確認し、テストを実行します。

表現力 > 証明

仕様を証明できるものに制限する理由はありませんが、それが主に型システムが行うことです。システムについて伝達し、検証したいことはたくさんあります。これは、構造的/表現的な型やタグ付けを超えて、例えば、ドメインを絞り込んだり、入力間または入力と出力の関係を詳しく述べたりする述語にまで及びます。さらに、私たちが最も気にするプロパティは、多くの場合、静的な概念ではなく、ランタイムの値のプロパティです。したがって、specは型システムではありません。

名前は重要

すべてのプログラムは、型システムがない場合でも名前を使用し、重要なセマンティクスをキャプチャします。Int x Int x Intは十分ではありません(長さ/幅/高さまたは高さ/幅/奥行きですか?)。したがって、specには、ラベルのないシーケンスコンポーネントやタグのない共用体バインディングはありません。このユーティリティは、specがユーザーに仕様について伝える必要がある場合(例えば、エラーレポートの場合)、また、ユーザーが仕様のジェネレーターをオーバーライドしたい場合(逆も同様)に明らかになります。すべてのブランチに名前が付けられている場合、パスを使用して仕様の一部について話すことができます。

グローバル(名前空間付き)の名前はさらに重要

Clojureは名前空間付きのキーワードとシンボルをサポートしています。ここで、Clojureの名前空間オブジェクトではなく、名前空間修飾名についてのみ話していることに注意してください。これらは悲劇的に十分に活用されておらず、辞書/データベース/マップ/セットで競合することなく常に共存できるため、重要な利点をもたらします。specでは、名前空間修飾されたキーワードとシンボル(のみ)を使用して仕様に名前を付けることができます。情報マップに名前空間付きのキーを使用している人々(私たちが成長を望む実践)は、それらの名前で直接それらの属性の仕様を登録できます。これにより、特に動的なコンテキストで、マップの自己記述がカテゴリー的に変更され、構成と一貫性が促進されます。

Clojureの(具体化された)名前空間にさらなる追加/オーバーロードをしない

var、メタデータなどには何も添付されません。すべての関数には名前空間付きの名前があり、他の場所に保存されている関連データ(例:仕様)へのキーとして使用できます。

コードはデータ(逆ではない)

Lisps(そしてClojure)では、コードはデータです。しかし、データは、その周囲に言語を定義するまでコードにはなりません。この分野の多くのDSLは、スキーマのデータ表現を目指しています。しかし、述語的な仕様にはオープンで大きな語彙があり、有用な述語のほとんどはすでに存在し、コアや他の名前空間の関数としてよく知られており、または単純な式として記述できます。これらの述語をすべて「データ化」し、場合によっては名前を変更することは、価値がほとんどなく、正確なセマンティクスを理解する上で明確なコストがかかります。代わりに、specは、元の述語と式がそもそもデータであるという事実を活用し、ドキュメントやエラー報告でユーザーと通信するためにそのデータをキャプチャします。はい、これはclojure.specの表面領域の多くがマクロになることを意味しますが、仕様は圧倒的に人が作成し、構成される場合は手動で行われます。

集合(マップ)は、メンバーシップに関するものです、それだけです

上記のように、キーの値の詳細を定義するマップは、サポートされない懸念事項の根本的な複雑化です。マップ仕様は、必須/オプションのキー(つまり、集合のメンバーシップに関するもの)を詳細に記述し、キーワード/属性/値のセマンティクスは独立しています。マップのチェックは2段階で行われ、まず必須キーの存在を確認し、次にキー/値の適合性を確認します。後者は、実行時に存在する(名前空間修飾された)キーがマップ仕様にない場合でも実行できます。これは、構成と動的性のために非常に重要です。

情報的対実装的

必然的に、人々は実装上の決定を詳細に記述するために仕様システムを使用しようとしますが、それは彼らの損失になります。最も良く、最も役立つ仕様(およびインターフェース)は、純粋に情報的な側面に向けられています。情報仕様のみが、ワイヤーを介して、およびシステム全体で機能します。私たちは常に情報アプローチを優先し、矛盾がある場合はそれを優先します。

K.I.S.S.

この分野には非常に少ない基本的な概念があり、それらに固執するように努めます。明確な構造的概念はわずかです。少数のアトミック型、シーケンシャルなもの、集合、マップです。当然のことながら、これらはClojureのデータ型であり、基本的な操作はこれらに対してのみ提供されます。同様に、これらについて説明するための数学的なツールがあります。マップの集合論とシーケンスの正規表現は、価値のある特性を持っています。私たちは、アドホックなソリューションよりもこれらを優先します。

test.checkを基盤とするが、その知識を必要としない

specの生成的なテストの基盤はtest.checkを活用し、それを再発明しません。しかし、specのユーザーは、独自のジェネレータを作成したり、specが生成したテストを独自のプロパティベースのテストで補強したりする場合を除き、test.checkについて何も知る必要はありません。test.checkへの本番環境でのランタイム依存関係は存在しないはずです。

特徴

概要

述語的な仕様

基本的な考え方は、仕様は述語の論理的な組み合わせにすぎないということです。基本的には、int?symbol?のような、使い慣れた単純なブール述語や、#(< 42 % 66)のように自分で作成した式について説明しています。specは、spec/andspec/orのような論理演算を追加し、仕様を論理的に結合し、詳細なレポート、生成、適合サポートを提供し、spec/orの場合はタグ付きの戻り値を提供します。

マップ

マップキーセットの仕様は、必須およびオプションのキーセットの仕様を提供します。マップの仕様は、キー名のベクターにマッピングする:reqおよび:optキーワード引数を指定してkeysを呼び出すことによって生成されます。

:reqキーは、論理演算子andorをサポートします。

(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つ以上の述語でさらに制約します

これらは任意にネストして、複雑な式を形成します。

cataltは、すべてのコンポーネントにラベルが付けられている必要があり、それぞれの戻り値は、一致したコンポーネントに対応するキーを持つマップであることに注意してください。このようにして、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}}

conform/explain

上記のように、仕様を使用するための基本的な操作はconformであり、仕様と値を受け取り、適合した値を返すか、値が適合しなかった場合は:clojure.spec.alpha/invalidを返します。値が適合しない場合は、explainまたはexplain-dataを呼び出して、適合しなかった理由を調べることができます。

仕様の定義

仕様を定義するための主な操作は、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を介して仕様を呼び出すことができます。

仕様の使用

ドキュメント

fdefを介して定義された関数仕様は、関数名でdocを呼び出すと表示されます。仕様でdescribeを呼び出して、フォームとして説明を取得できます。

解析/デストラクチャリング

実装でconformを直接使用して、デストラクチャリング/解析/エラーチェックを取得できます。conformは、例えばマクロの実装やI/O境界で使用できます。

開発中

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 の呼び出しでラップする必要があることに注意してください。そうしないと、ネストされたパターンと見なされます。

スペック

spec, and, or, および keys の戻り値。

正規表現操作

cat, alt, *, +, ?, & の戻り値。ネストすると、これらは単一の式を形成します。

conform

conform はスペックを使用するための基本的な操作であり、検証と適合/デストラクチャリングの両方を行います。適合は「深い」ものであり、スペックと正規表現操作、マップスペックなどをすべて通過することに注意してください。nilfalse は正当な適合値であるため、値を適合させることができない場合、conform は区別された :clojure.spec.alpha/invalid を返します。valid? は完全なブール値述語として代わりに使用できます。

explain

値がスペックに適合しない場合、同じスペック+値で explain または explain-data を呼び出して理由を確認できます。これらの説明は、追加の作業を実行する可能性があり、失敗しない入力やレポートが不要な場合にそのコストを負担する理由がないため、conform 中には生成されません。説明の重要な要素は _パス_ です。explain は、例えばネストされたマップや正規表現パターンをナビゲートする際にパスを拡張するため、全体またはリーフの値だけでなく、より良い情報を取得できます。explain-data は、問題へのパスのマップを返します。

パス

スペックのすべての _分岐_ 点は、マップの keysoralt の選択、および (場合によっては省略される) cat の要素など、ラベル付けされているため、スペックのすべての部分式は、パーツの名前を付ける _パス_ (キーのベクター) を介して参照できます。これらのパスは、explaingen のオーバーライド、およびさまざまなエラーレポートで使用されます。

先行技術

スペックについて斬新なものはほとんどありません。上記のすべてのライブラリ、RDF、および Racket のコントラクト など、さまざまなコントラクトシステムで行われたすべての作業を参照してください。

スペックが便利で強力であることを願っています。

Rich Hickey