Skip to content

Instantly share code, notes, and snippets.

@mmzsource
Last active April 20, 2017 07:54
Show Gist options
  • Save mmzsource/abe6d65a3b0e27ffb348f8c091e03a7d to your computer and use it in GitHub Desktop.
Save mmzsource/abe6d65a3b0e27ffb348f8c091e03a7d to your computer and use it in GitHub Desktop.
Trying clojure.spec to check, document & generate a league-table datastructure
;; 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