This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2022-07-05
Channels
- # announcements (1)
- # asami (21)
- # aws (19)
- # babashka (37)
- # beginners (38)
- # clj-kondo (7)
- # clj-otel (8)
- # clojure (29)
- # clojure-europe (54)
- # clojure-nl (3)
- # clojure-spec (2)
- # clojure-uk (2)
- # clojurescript (15)
- # conjure (1)
- # data-science (1)
- # datomic (21)
- # emacs (6)
- # events (3)
- # figwheel-main (1)
- # gratitude (13)
- # holy-lambda (11)
- # joyride (6)
- # klipse (3)
- # malli (14)
- # missionary (26)
- # nbb (31)
- # omni-trace (2)
- # pathom (3)
- # reagent (1)
- # reitit (1)
- # releases (1)
- # shadow-cljs (24)
- # sql (27)
- # tools-deps (4)
- # vim (21)
How to model a deck of cards? We use spec at work, but I wanted to try Malli for a personal toy project. I promptly ran into a problem I couldn't solve, and I've tried to make a simplified version of it here. Imagine a simplified game of poker: a hand is 5 cards, and we only care about whether it is a flush (i.e. 5 cards of the same suit) or not.
First attempt As we don't really care about the numbers or faces we can model a card as simple enum of its suit, and a hand as a 5-tuple of cards. The deck is trickier. It has up to 52 cards, but I can't find a way to model the additional constraint that we should only have a maximum of 13 of each suit. Thus the last test in this self-contained example (usually) fails:
(ns cards.simple-test
(:require
[clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.generator :as mg]))
(def Card [:enum :spades :hearts :clubs :diamonds])
(def Hand [:tuple Card Card Card Card Card])
(def Deck [:sequential {:max 52} Card])
(deftest deck
(testing "we can generate a Deck"
(doseq [deck (mg/sample Deck)]
(testing "that validates"
(is (true? (m/validate Deck deck))))
(testing "where every element is a card"
(is (every? #(m/validate Card %) deck)))
(testing "of maximum 52 cards"
(is (>= 52 (count deck))))
(testing "where each suite occurs at most 13 times"
;; this test (usually) fails, because we haven't restricted
;; the count of each suit to maximum 13.
(is (>= 13 (->> deck
frequencies
vals
(reduce max 0))))))))
Second attempt
Let's give each card a suit and number to address the problem with our previous deck.
A hand is now slighly more complicated: we have to use a set with {:min 5 :max 5}
properties instead of a 5-tuple to ensure cards are not duplicated. (A hand with 5 ace of spades should not be valid!)
The deck, on the other hand, is simpler than before. By using a set we can drop the {:max 52}
property. It is now implicit, because there are only 52 possible combinations of 4 suits and 13 numbers.
Self-contained example:
(ns cards.better-test
(:require
[clojure.test :refer [deftest is testing]]
[malli.core :as m]
[malli.generator :as mg]))
(def Card [:tuple
[:enum :spades :hearts :clubs :diamonds]
[:int {:min 1 :max 13}]])
(def Hand [:set {:min 5 :max 5} Card])
(def Deck [:set Card])
(deftest deck
(testing "we can generate a Deck"
(doseq [deck (mg/sample Deck)]
(testing "that validates"
(is (true? (m/validate Deck deck))))
(testing "where every element is a card"
(is (every? #(m/validate Card %) deck)))
(testing "of maximum 52 cards"
(is (>= 52 (count deck))))
(testing "where each suite occurs at most 13 times"
(is (>= 13 (->> deck
(map first)
frequencies
vals
(reduce max 0))))))))
This solves the problem with my first attempt, but introduces another: we can neither sort nor properly shuffle our deck now. ๐
I think what I want is an ordered sequence of distinct values, but that may be because I can't see outside my Specs-shaped box. Is there an alternative way to model this with Malli?
(def Deck [:sequential {:distinct true} Card])
Define card as tuple of suit and number up to 13. A deck is a distinct sequence of cards. Unshuffled deck is a sorted deck
That sounds like my second attempt:
(def Card [:tuple
[:enum :spades :hearts :clubs :diamonds]
[:int {:min 1 :max 13}]])
(def Hand [:set {:min 5 :max 5} Card])
(def Deck [:set Card])
How can I specify a distinct sequence? It seems you have to go to a set, as thereโs no (documented) {:distinct true}
property I can add.
Ah, I think I see. I just discovered :fn
๐
This appears to do what I want:
(def Card [:tuple
[:enum :spades :hearts :clubs :diamonds]
[:int {:min 1 :max 13}]])
(def Hand [:and
[:sequential {:min 5 :max 5} Card]
[:fn (fn [hand] (apply distinct? hand))]])
(def Deck [:and
[:sequential Card]
[:fn (fn [deck] (apply distinct? deck))]])
That can be made to work with my simplified version too:
(def Card [:enum :spades :hearts :clubs :diamonds])
(def Hand [:sequential {:min 5 :max 5} Card])
(def Deck [:and
[:sequential Card]
[:fn #(->> % frequencies vals (reduce max 0) (>= 13))]])
Thank you for the help ๐http://malli.io not working?