(spec/keys :req [::x ::y (or ::secret (and ::user ::pwd))] :opt [::z])
Clojure 是動態語言。這表示許多事情,包括執行程式碼不需要型別註解。儘管 Clojure 支援一些型別提示,但它們不是強制機制,也不全面,且僅限於傳達資訊給編譯器,以協助產生有效率的程式碼。Clojure 會透過 JVM 本身執行更豐富型別的執行時期檢查。
然而,將資訊單純表示為資料一直是 Clojure 的指導原則,社群廣泛重視並實踐這項原則。因此,Clojure 系統的重要屬性會透過資料的形狀和其他謂詞屬性來表示和傳達,不會在任何地方擷取或檢查,因為執行時期型別是無法區分的異質映射和向量。
大多數用於指定結構的系統會將鍵集的規格(例如映射中的鍵、物件中的欄位)與那些鍵指定的數值的規格混為一談。也就是說,在這種方法中,映射的架構可能會說:a 鍵的型別是 x 型別,b 鍵的型別是 y 型別。這是僵化和冗餘的主要來源。
在 Clojure 中,我們透過動態組成、合併和建構映射來獲得力量。我們例行處理由不可靠外部來源產生的選用和部分資料、動態查詢等。這些映射表示相同鍵的不同集合、子集合、交集和聯集,且通常在任何使用的地方都應具有相同的語意。定義每個子集合/聯集/交集的規格,然後重複陳述每個鍵的語意,既是一種反模式,在最動態的情況下也無法執行。
許多使用者,特別是初學者,會對手寫剖析和解構程式碼產生的錯誤訊息感到沮喪和挑戰,特別是在巨集中有兩個執行內容(巨集在編譯時期執行,其擴充在執行時期執行,任何一個都可能因使用者錯誤而失敗)。這導致有人要求「巨集語法」,但事實上巨集只是資料→資料的函式,任何資料驗證和解構的解決方案都應能像其他函式一樣適用於它們。也就是說,巨集是上述問題的一個實例。
最後,在所有語言中,動態或非動態,測試對於品質至關重要。許多關鍵屬性無法透過常見的類型系統擷取。但手動測試的效能/努力比非常低。基於屬性的產生式測試,正如在 test.check 中對 Clojure 實作的那樣,已被證明遠比手動撰寫的測試更強大。
然而,基於屬性的測試需要定義屬性,這需要額外的努力和專業知識才能產生,而且在函式層級上,與函式規格有大量的重疊。函式層級中的許多有趣屬性已經可以透過結構化 + 謂詞規格擷取。理想情況下,規格應與產生式測試整合,並「免費」提供特定類別的產生式測試。
我們並不 (也無法) 生活在一個不會犯錯的世界裡。相反地,我們會定期檢查我們是否沒有犯錯。亞馬遜並不會透過 UPS<Trucks<Boxes<TV>>>
將您的電視寄給您。因此,您偶爾可能會收到微波爐,但供應鏈並不會因此而負擔正確性的證明。相反地,我們會在邊緣檢查並執行測試。
沒有理由將我們的規格限制在我們可以證明的事物上,但這正是類型系統主要在做的事。我們希望傳達和驗證我們系統的更多內容。這超越了結構/表示類型和標記,而進入到例如縮小網域或詳細說明輸入之間或輸入與輸出之間關係的謂詞。此外,我們最關心的屬性通常是執行時期值的屬性,而不是某些靜態概念。因此,規格不是一個類型系統。
所有程式都使用名稱,即使類型系統沒有使用,而且它們會擷取重要的語意。Int x Int x Int
根本不夠好 (它是長度/寬度/高度還是高度/寬度/深度?)。因此,規格不會有未標籤的序列元件或未標籤的聯集繫結。當規格需要與使用者討論規格時,例如在錯誤報告中,反之亦然,例如當使用者想要覆寫規格中的產生器時,這種效用就會變得明顯。當所有分支都已命名時,您可以使用路徑來討論規格的部分。
Clojure 支援命名空間關鍵字和符號。請注意,我們在此僅討論命名空間限定的名稱,而非 Clojure 命名空間物件。這些功能嚴重未被善用,且傳達了重要的優點,因為它們始終可以在字典/資料庫/映射/集合中並存而不會發生衝突。spec 將允許(僅)命名空間限定的關鍵字和符號來命名規格。將命名空間鍵用於其資訊映射的人員(我們希望看到這種做法的成長)可以直接在這些名稱下註冊這些屬性的規格。這會徹底改變映射的自我描述,特別是在動態環境中,並鼓勵組合和一致性。
在 Lisp(因此 Clojure)中,程式碼是資料。但在定義其周圍的語言之前,資料並非程式碼。在此領域中的許多 DSL 會針對架構驅動資料表示。但預測規格具有開放且廣泛的詞彙,而且大多數有用的謂詞已存在,並在核心和其他命名空間中以函數的形式廣為人知,或者可以寫成簡單的表達式。必須「資料化」所有這些謂詞(可能會重新命名),這幾乎沒有增加價值,而且在理解精確語意方面必定會付出代價。spec 反而利用了原始謂詞和表達式一開始就是資料的事實,並擷取該資料以用於與使用者在文件和錯誤報告中進行通訊。是的,這表示 clojure.spec
的更多表面積將會是巨集,但規格絕大部分都是由人員編寫,而且在組合時也是手動編寫。
根據上述,定義其鍵值詳細資訊的地圖是關注事項的基本組成,將不會受到支援。地圖規格詳述必要/選用鍵(例如設定成員資格事項),而關鍵字/屬性/值語意是獨立的。地圖檢查為兩階段,首先是必要鍵的存在,然後是鍵/值的一致性。後者可以在執行階段不存在(名稱空間限定)鍵時執行。這對於組成和動態性至關重要。
人們總是會嘗試使用規格系統來詳述實作決策,但他們這麼做對自己有害無益。最佳且最有用的規格(和介面)與純資訊面向相關。只有資訊規格才能透過網路和跨系統運作。我們將永遠優先考量資訊方法,並在發生衝突時優先採用資訊方法。
基本概念是,規範只不過是謂詞的邏輯組合。在最底層,我們討論的是你習慣使用的簡單布林謂詞,例如 int?
或 symbol?
,或你自行建立的表達式,例如 #(< 42 % 66)
。spec 新增了邏輯運算,例如 spec/and
和 spec/or
,它們以邏輯方式結合規範,並提供深入的報告、產生和符合支援,以及在 spec/or
的情況下,標記回傳。
對應鍵集的規範提供對應所需和選用鍵集的規範。對應的規範是透過呼叫 keys
產生,其中 :req
和 :opt
關鍵字引數對應到鍵名稱的向量。
:req
鍵支援邏輯運算子 and
和 or
。
(spec/keys :req [::x ::y (or ::secret (and ::user ::pwd))] :opt [::z])
spec 和其他系統之間最明顯的差異之一是,對應規範中沒有指定 值 的位置,例如 ::x
可以接受。spec 的(強制)意見是,與命名空間關鍵字相關聯的值的規範,例如 :my.ns/k
,應註冊在該關鍵字本身下,並套用在該關鍵字出現的任何對應中。這有許多優點
它確保在所有使用該關鍵字的應用程式中一致,因為所有使用都應共用一個語意
它也確保程式庫和使用者之間的一致性
它減少了重複,因為否則許多對應規範需要對 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
- 一組謂詞/模式中的一個選擇
*
- 謂詞/模式的零次或多次出現
+
- 一次或多次
?
- 一次或沒有
&
- 採用正規表示式運算並進一步使用一個或多個謂詞來限制它
這些可以任意巢狀以形成複雜的表達式。
請注意,cat
和 alt
要求其所有組成部分都標有標籤,並且每個組成部分的回傳值都是一個映射,其鍵對應於匹配的組成部分。通過這種方式,規格正規表示式充當解構和解析工具。
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
。
為了讓一個規格可以透過名稱重複使用,必須透過 def
註冊。def
採用一個命名空間限定的關鍵字/符號和一個規格/謂詞表達式。根據慣例,資料規格應註冊在關鍵字下,而屬性值應註冊在其屬性名稱關鍵字下。註冊後,可以在任何 規格操作中需要規格/謂詞的地方使用該名稱。
函數可透過三個規格完全指定 - 一個用於參數、一個用於回傳值,以及一個用於函數運算,將參數與回傳值關聯起來。
函數的參數規格永遠會是一個正規表示法,將參數指定為一個清單,也就是說,清單會傳遞給函數的 apply
。透過這種方式,單一規格可以處理具有多個元數的函數。
回傳值規格是單一值的任意規格。
(選擇性的) 函數規格是參數與回傳值之間關係的進一步說明,也就是說,函數的函數。它會傳遞 (例如在測試期間) 一個包含 {:args conformed-args :ret conformed-ret}
的映射,並且通常會包含將這些值關聯起來的謂詞 - 例如,它可以確保輸入映射的所有鍵都存在於回傳的映射中。
您可以在單一呼叫 fdef
中完全指定函數的所有三個規格,並透過 fn-specs
呼叫規格。
您可以選擇性地使用 instrument
儀器化函數和命名空間,它會將函數變數替換為函數的包裝版本,測試 :args
規格。unstrument
會將函數回傳到其原始版本。您可以使用 gen/sample
產生資料以進行互動式測試。
規範 API 的許多部分呼叫「謂詞」或「preds」。這些引數可以透過以下方式滿足
謂詞 (布林) fns
集合
已註冊的規範名稱
規範 (spec
、and
、or
、keys
的回傳值)
正規運算式運算 (cat
、alt
、*
、+
、?
、&
的回傳值)
請注意,如果你想在正規運算式中巢狀獨立的正規運算式謂詞,你必須將其包裝在對 spec
的呼叫中,否則它將被視為巢狀模式。
spec
、and
、or
和 keys
的回傳值。
cat
、alt
、*
、+
、?
、&
的回傳值。巢狀時,這些會形成單一表達式。
conform
是使用規範的基本運算,並執行驗證和符合/解構。請注意,符合是「深入」的,並流經所有規範和正規運算式運算、對應規範等。由於 nil
和 false
是合法的符合值,因此當無法使值符合時,conform
會回傳特別的 :clojure.spec.alpha/invalid
。valid
? 可以用作完全布林的謂詞。
當值無法符合規範時,你可以使用相同的規範 + 值呼叫 explain
或 explain-data
來找出原因。這些說明不會在 conform
期間產生,因為它們可能會執行其他工作,而且沒有理由讓非失敗輸入或不需要報告時承擔這些成本。說明的重要組成部分是路徑。explain
在瀏覽巢狀對應或正規運算式模式等時會延伸路徑,因此你會獲得比整個或葉值更好的資訊。explain-data
會回傳路徑到問題的對應。
由於規範中所有分支點都標有標籤,即對應 keys
、or
和 alt
中的選項,以及 cat
的(可能省略的)元素,因此規範中的每個子表達式都可以透過路徑(命名部分的鍵向量)來參照。這些路徑用於 explain
、gen
覆寫和各種錯誤報告。