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
object DeckOfCards Example extends App { | |
//clumsy enumeration definition | |
sealed abstract class Suite | |
case object Spade extends Suite | |
case object Heart extends Suite | |
case object Club extends Suite | |
case object Diamond extends Suite | |
sealed abstract class Rank | |
case object Two extends Rank | |
case object Three extends Rank | |
case object Four extends Rank | |
case object Five extends Rank | |
case object Six extends Rank | |
case object Seven extends Rank | |
case object Eight extends Rank | |
case object Nine extends Rank | |
case object Ten extends Rank | |
case object Jack extends Rank | |
case object Queen extends Rank | |
case object King extends Rank | |
case object Ace extends Rank | |
val suites = Set(Spade, Heart, Club, Diamond) | |
val ranks = List(Two, Three, Four, Five, Six, Seven, Eight, Nine, Ten, Jack, Queen, King, Ace) | |
//the interesting part | |
case class Card(rank: Rank, suite: Suite) | |
class Deck(pCards: List[Card] = for (r <- ranks; s <- suites) yield Card(r, s)) { | |
val cards = if (isValidDeck(pCards)) pCards | |
else throw new RuntimeException("Deck is invalid!") | |
def shuffle() = new Deck(Random.shuffle(cards)) | |
def pullFromTop() = (cards.head, new Deck(cards.tail)) | |
def addToTop(card: Card) = new Deck(card :: cards) | |
def addToTop(cardsToAdd: List[Card]) = new Deck(cardsToAdd ::: cards) | |
private def isValidDeck(cards: List[Card]) = cards.size <= 52 && cards.distinct.size == cards.size | |
} | |
} |
See the check in the "constructor" and the isValidDeck function. This ensures that our Deck is always "valid". All the code using this class can safely assume that it doesn't contain duplicate cards. It's the simplest invariant I could come up, but serves the purpose of the post well. If I try to get out of my default OO-mindset acquired in the last 10 years and look at it with fresh eyes, what I see that we have a "raw" data structure, a list, and certain constraints attached to it. The data structure and the constraints together are the class.
Class = raw data structure + constraints
The state-space of the Deck object obviously couldn't be bigger than that of the list, the raw data. So building a class around a raw data structure is simply imposing restrictions on how the underlying data can be modified by the clients of the class. Of course the methods of a class can hide side-effects, too, but again, in the aspect of invariants, it's not relevant.
The benefit of the OO approach is that the Deck stands guard over its invariants, so the constrains and the data "travel together". The disadvantage is that we lose all the already existing data-manipulating functions the language would provide out of the box. Let's demonstrate it with the Clojure solution.
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
(defn suites [] | |
[:heart :spade :diamond :club]) | |
(defn ranks [] | |
[:2 :3 :4 :5 :6 :7 :8 :9 :10 :J :Q :K :A]) | |
;;represent the deck as a list and the cards as tupples (simply a 2-element collection in Clojure) | |
(defn new-deck [] | |
(for [s (suites) | |
r (ranks)] [r s])) | |
;;it's a full deck | |
(def a-deck (new-deck)) | |
;;THE REQUIRED OPERATIONS PROVIDED BY THE BUILT-IN LANGUAGE FUNCTIONS | |
;;shuffle the deck | |
(shuffle a-deck) | |
;;add cards on the top of the deck | |
(conj (new-deck) [:2 :spade] [:J :club]) | |
;;pull the top card. Returns the card and the rest | |
[(peek a-deck) (pop a-deck)] |
The disadvantage of Clojure's lightweight way is that we now lost the guard on the invariants. You can pass an arbitrary list to any functions expecting a valid deck, opening the door for hard to detect bugs. However I think there is a remedy for this and quite an easy one of that.
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
;;validator function | |
(defn is-valid-deck? [deck] | |
(and (>= 52 (count deck)) | |
(distinct? deck))) | |
;;another validator function | |
(defn is-full-deck? [deck] | |
(= 52 (count deck))) | |
;;here comes the magic | |
(defn with-invariant [invariant f in] | |
{:pre [(invariant in)] | |
:post [(invariant %)]} | |
"Takes a validator function, a function and the input. Validates both | |
the input and the output with the validator" | |
(f in)) | |
;;this will throw an exception | |
(with-invariant is-full-deck? rest (new-deck)) | |
;;this will pass | |
(with-invariant distinct? rest (new-deck)) | |
;;if we want to use the same invariant with a lot of operations on the deck, we can create a new function for it | |
(defn with-valid-deck-invariant [f deck] | |
(with-invariant is-valid-deck? f deck)) | |
;;will pass if the deck doesn't contain 'card' yet | |
(with-valid-deck-invariant #(conj % card) deck) | |
Classes vs (raw data + validator functions) = safety vs reusability
Imagine now we want to extend our game of cards program by allowing different kind of games, including ones where the deck can contain duplicates. Or requires different operations on the deck (like pull from the bottom). Can we reuse the Deck class? Not in this current form. Maybe subclassing it or extract some traits? Either way the code starts to get complicated.
With Clojure we just use the same basic data structure (list) a new validator function with with-invariant or build it up in one single function for brevity's sake. The Clojure implementation dissected the class and cleaved off the constraints from the data. The data structure can processed in almost every imaginable way with the rich set of functions built-in the language, and the constraints can be plugged-in the computation whenever needed.
No comments :
Post a Comment