Last active
April 20, 2017 07:54
-
-
Save mmzsource/abe6d65a3b0e27ffb348f8c091e03a7d to your computer and use it in GitHub Desktop.
Trying clojure.spec to check, document & generate a league-table datastructure
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; REPL session to play around with clojure.spec | |
;; ------------- START INITIAL SETUP ------------- | |
;; assume these dependencies are specified in the repl environment: | |
;; [org.clojure/clojure "1.9.0-alpha15"] | |
;; [org.clojure/test.check "0.9.0"] | |
;; My leiningen setup: | |
;; cat ~/.lein/profiles.clj | |
;; {:repl | |
;; {:dependencies [ | |
;; ^:displace [org.clojure/clojure "1.9.0-alpha15"] | |
;; [org.clojure/test.check "0.9.0"] | |
;; ]} | |
;; } | |
;; To make sure I'm running from a clean environment: | |
;; rm -rf ~/.m2/repository/org/clojure/ | |
;; lein repl | |
;; ------------- END INITIAL SETUP ------------- | |
;; A league table looks something like this: | |
;; | pos | team | games | win | draw | loss | pts | goals | against | | |
;; |-----+------+-------+-----+------+------+-----+-------+---------+ | |
;; | 1 | FEY | 26 | 21 | 3 | 2 | 66 | 66 | 16 | | |
;; | 2 | AJA | 26 | 19 | 5 | 2 | 62 | 55 | 16 | | |
;; | 3 | PSV | 26 | 17 | 7 | 2 | 58 | 51 | 18 | | |
;; ...etc | |
;; Without the column names, a corresponding data structure for the league table could be: | |
;; [[1 :FEY 26 21 3 2 66 66 16] | |
;; [2 :AJA 26 19 5 2 62 55 16] | |
;; [3 :PSV 26 17 7 2 58 51 18]] | |
;; This makes sense if you read this documentation and while working with the code. | |
;; However, when looking at the code in a couple of weeks / months from now, | |
;; one might wonder what the hell those positional vector items actually mean. | |
;; Therefore, I want to create a spec which helps me check, document & generate | |
;; this league-table datastructure | |
(require '[clojure.spec :as s]) | |
(require '[clojure.spec.gen :as gen]) | |
;; highest league position is nr 1, lowest is nr 20 (assuming 20 clubs) | |
(s/def ::position (s/int-in 1 21)) | |
;; possible team keywords | |
(s/def ::teams #{:AJA :FEY :PSV}) | |
;; lowest nr of games is 0, highest is 38 (assuming 20 clubs) | |
(s/def ::games (s/int-in 0 39)) | |
;; nr of possible wins is equal to nr of possible games | |
(s/def ::wins ::games) | |
;; nr of possible draws is equal to nr of possible games | |
(s/def ::draws ::games) | |
;; nr of possible losses is equal to nr of possible games | |
(s/def ::losses ::games) | |
;; max 3 points per game in max 38 games. 38 X 3 = 114 | |
(s/def ::points (s/int-in 0 115)) | |
;; arbitrary number of maximum goals scored | |
(s/def ::goals (s/int-in 0 256)) | |
;; arbitrary number of goals against | |
(s/def ::goals-against ::goals) | |
;; Now let's compose a spec for the datastructure. | |
;; If I understand the documentation https://clojure.org/guides/spec#_collections | |
;; correctly, I have 2 options to compose my spec in this context: | |
;; - Regular expression | |
;; - Tuple | |
;; First let's try the tuple: | |
(s/def ::league-score-tuple | |
(s/tuple | |
::position ::teams ::games ::wins ::draws ::losses ::points ::goals ::goals-against)) | |
(s/conform ::league-score-tuple [3 :PSV 26 17 7 2 58 51 18]) | |
;; => [3 :PSV 26 17 7 2 58 51 18] | |
(gen/sample (s/gen ::league-score-tuple) 10) | |
;; => ([1 :PSV 1 1 1 1 1 1 1] | |
;; [1 :AJA 1 1 1 0 1 1 1] | |
;; [1 :FEY 2 1 2 1 1 1 1] | |
;; [1 :FEY 3 0 3 1 0 0 1] | |
;; [3 :PSV 3 3 1 0 0 0 4] | |
;; [7 :PSV 2 1 0 3 1 2 1] | |
;; [2 :PSV 3 1 4 4 2 7 2] | |
;; [2 :PSV 0 4 3 21 6 7 1] | |
;; [4 :FEY 1 0 10 4 1 31 2] | |
;; [2 :PSV 20 27 2 0 8 229 54]) | |
;; Now let's try the regular expression: | |
(s/def ::league-score-reg | |
(s/cat :pos ::position | |
:team ::teams | |
:games ::games | |
:win ::wins | |
:draw ::draws | |
:loss ::losses | |
:pts ::points | |
:goals ::goals | |
:against ::goals-against)) | |
(s/conform ::league-score-reg [1 :FEY 26 21 3 2 66 66 16]) | |
;; => {:pos 1, :team :FEY, :games 26, :win 21, :draw 3, :loss 2, :pts 66, :goals 66, :against 16} | |
(gen/sample (s/gen ::league-score-reg) 10) | |
;; => ((1 :FEY 1 1 1 0 1 1 1) | |
;; (1 :PSV 0 0 0 1 0 1 0) | |
;; (1 :AJA 1 1 1 1 1 1 0) | |
;; (3 :PSV 1 1 2 2 1 1 0) | |
;; (1 :FEY 1 1 0 1 2 6 0) | |
;; (2 :PSV 11 1 2 2 1 1 7) | |
;; (1 :PSV 8 3 2 10 3 2 0) | |
;; (19 :AJA 25 0 4 9 7 1 28) | |
;; (5 :FEY 1 2 2 1 1 8 1) | |
;; (1 :PSV 5 19 0 1 1 73 13)) | |
(s/conform ::league-score-reg [1 :PSV 1 2 3 4 5 6 7 8]) | |
;; => :clojure.spec/invalid | |
(s/explain ::league-score-reg [1 :PSV 1 2 3 4 5 6 7 8]) | |
;; In: [9] val: (8) fails spec: :soccer-bet-stats.core/league-score-reg predicate: (cat :pos :soccer-bet-stats.core/position :team :soccer-bet-stats.core/teams :games :soccer-bet-stats.core/games :win :soccer-bet-stats.core/wins :draw :soccer-bet-stats.core/draws :loss :soccer-bet-stats.core/losses :pts :soccer-bet-stats.core/points :goals :soccer-bet-stats.core/goals :against :soccer-bet-stats.core/goals-against), Extra input | |
;; => nil | |
(s/explain ::league-score-reg [1 :PSV 1 2 3 4 5 6 7]) | |
;; Success! | |
;; => nil | |
(s/explain ::league-score-reg [1 :PSV 1 2 3 4 5 6 7000]) | |
;; In: [8] val: 7000 fails spec: :soccer-bet-stats.core/goals at: [:against] predicate: (int-in-range? 0 256 %) | |
;; => nil | |
(s/explain ::league-score-reg [1 :PSV 1 2 3 4 5 6]) | |
;; val: () fails spec: :soccer-bet-stats.core/goals at: [:against] predicate: :soccer-bet-stats.core/goals-against, Insufficient input | |
;; => nil | |
(s/explain ::league-score-reg [1 :FOO 1 2 3 4 5 6 7]) | |
;; In: [1] val: :FOO fails spec: :soccer-bet-stats.core/teams at: [:team] predicate: #{:PSV :FEY :AJA} | |
;; => nil | |
(s/explain ::league-score-reg [1 :PSV "bla" 2 3 4 5 6 7]) | |
;; In: [2] val: "bla" fails spec: :soccer-bet-stats.core/games at: [:games] predicate: int? | |
;; => nil | |
;; Some 'conclusions' after this spike: | |
;; - The specs are very readable and easily composable | |
;; - The sample generation feels like magic | |
;; - The regular expression `s/cat` generates / documents the | |
;; intended meaning of my positional vector items when needed | |
;; (for instance in a couple of weeks / months when looking at the code anew) | |
;; - It's all very declarative and close to the domain | |
;; - (without the comments:) in a couple of lines of code I declared a validator, | |
;; documentation and generator for my dynamically spiked datastructure | |
;; and made a robust building block. :D | |
;; - looking forward generating some tests with it |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment