Andalib Ali
Andalib Ali

Reputation: 61

Use integer values for constructing another data type in Haskell

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

Answers (3)

Erik Kaplun
Erik Kaplun

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

Viktor Mellgren
Viktor Mellgren

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

Thomas M. DuBuisson
Thomas M. DuBuisson

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

Related Questions