Reputation: 61
I am trying to define a data type that has the ranks of a playing card.
data Suit = Spades | Clubs | Hearts | Diamonds deriving Show
data Rank = 2|3|4|5|6|7|8|9|10 | Jack | Queen | King | Ace deriving Show
data Card = Card Rank Suit
I am having trouble incorporating the numbers [2..10]
as a rank, I did try to replace the numbers with LowerRanks Int
, it would run the code fine, but It does not allow me to call out any numbers.
data Suit = Spades | Clubs | Hearts | Diamonds deriving Show
data Rank = LowerRank Int | Jack | Queen | King | Ace deriving Show
data Card = Card Rank Suit
instance Show Card where
show (Card x y) = show x ++ " of " ++ show y
I can write:
Card Jack Spades
and i'll get Jack of Spades
. However, when I try to write Card 2 Spades
, I get an error:
No instance for (Num Rank) arising from the literal ‘2’
In the first argument of ‘Card’, namely ‘2’
Upvotes: 1
Views: 919
Reputation: 38217
Basically all of this boils down to some simple principles. I will also shed light on some fundamental design patterns at play here later on.
Data types are individual, independent sets of values obtained via constructors, which, figuratively, are magic functions without a defining body that "spawn" values ex nihilo, sort of:
data DataType = Constr1 | Constr2
sometimes you want to embed values from another type into the values of your type; you do that via parametric constructors:
data DataType = Constr1 | Constr2 | Constr3 Int
now values of type Int
can be embedded within the values of DataType
, but only as part of values constructed with Constr3
.
Now, what should be becoming obvious, is that you can't simply put values of type Int
directly into the set defined by DataType
:
data DataType = Constr1 | Constr2 | Constr3 Int | 1 | 2 | 3
— no, that does not work because that would lead to weird stuff like the values 1
, 2
and 3
being of the type DataType
, while it's obvious they are (also) of type Int
:
1 :: Int
1 :: DataType -- what you are attempting
while at the same time, some other values in Int
are not in DataType
:
4 :: DataType -- you want to AVOID this.
so as you can see, this has quickly led us to absurdity. And that's exactly what you were unknowingly referring to when you said "it doesn't allow me to call out any numbers".
So let's go back to "wrapping" constructors as already exemplified by Constr3
:
data Rank = Lower Int | Jack | Queen | King | Ace
what this allows us to do is:
Jack :: Rank -- of course
Ace :: Rank -- of course
Lower 3 :: Rank -- obviously
but also
Lower 999 :: Rank -- what?
which almost completely defeats the purpose of having a type-safe Rank
altogether, so we might as well just use integers 2 throughout 14 to indicate any rank, giving up static type safety — but we don't want that, as we'd then have to be concerned with avoiding running into anything below 2 or above 14, ending up sprinkling the code with boring, uninformative and unnecessary runtime checks.
Hence, while a solution such as this techically solves the issue — there is no compilation error and you can write your program — you'd constantly be on the verge, in need of ensuring there aren't some lower ranks gone rogue, living somewhere in your card game, pretending to be 999
or 1 000 000
and so on.
So here's the solution that I'd recommend instead:
data Rank = R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9 | R10
| Jack | Queen | King | Ace
deriving Show
this way you will never ever have to check the validity of a Rank
value once it's been constructed. You can always safely pattern match over ranks without getting spurious pattern exhaustion warnings at compile time, or pattern match failures at runtime.
To sum up, this "pattern" of design is often referred to as Correctness by Construction, Correctness by Design, Type Guided programming as well as the phrase "Make illegal states unrepresentable" etc. This design philosophy is also used in F#, OCaml, Scala and other statically typed programming languages (especially functional ones).
Furthermore: now that you have a nice flat and safe Rank
data type, you might want to consider making your Rank
an instance of Eq
and Ord
, getting free comparisons:
data Rank = R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9 | R10
| Jack | Queen | King | Ace
deriving (Eq, Ord, Show)
yielding:
Prelude> R2 == R3
False
Prelude> Ace > Queen
True
Prelude> R9 < Jack
True
Prelude> R9 <= R9
True
or if you want to be able to convert to and from integers, take a look also at Enum
.
Upvotes: 4
Reputation: 4506
data Rank = Numeric Integer | Jack | Queen | King | Ace
deriving (Eq, Show)
also supplies a how to randomly create them
instance Arbitrary Rank where
arbitrary = frequency [ (1, return Jack)
, (1, return Queen)
, (1, return King)
, (1, return Ace)
, (9, do n <- choose (2, 10)
return (Numeric n))
]
borrowed from a lab from Chalmers where you create a Black Jack game http://www.cse.chalmers.se/edu/course/TDA555/Code/Lab2/Cards.hs
Upvotes: 0
Reputation: 64740
Data constructors must begin with a capital letter or a colon, numbers are not valid constructors. You can instead write out the ranks, such as:
data Rank = Two | Three | Four | ...
As you noted, you can have an integral field too:
data Rank = RNum Int | Jack | Queen | King | Ace
Side note: I don't understand the problem "it doesn't allow me to call out any numbers". In the future please post the actual code you tried and error message. Feel free to clarify and I'll edit this answer.
With this second version of Rank
we can construct each card such as:
twoH = Card (RNum 2) Hearts
threeD = Card (RNum 3) Dimonds
It would be worth making a custom bounded instance for the rank and deriving bounded for Card, Suit. Deriving Ord
would be good too, but you'd want to place the suits in proper order for most card games.
Upvotes: 7