Search code examples
haskelltypesenumeration

Haskell way to go about enums


I want to represent a type of the following form :

(Card, Suit)

to represent cards in a card game where Card instances would be in the set:

{2, 3, 4, 5, 6, 7, 8, 9, J, Q, K, 1}

and Suit would have instances in the set:

{S, D, H, C}

I'd handle that with two Data declarations if that wasn't for the numbers:

data Suit = S | D | H | C deri...

but obviously adding numbers to those null arity types will fail.

So my question is, how to simulate the kind of enum you find in C?

I guess I'm misundestanding a basic point of the type system and help will be appreciated!

EDIT: I'll add some context: I want to represent the data contained in this Euler problem, as you can check, the data is represented in the form of 1S for an ace of spade, 2D for a 2 of diamond, etc...

What I'd really like is to be able to perform a read operation directly on the string to obtain the corresponding object.


Solution

  • I actually happen to have an implementation handy from when I was developing a poker bot. It's not particularly sophisticated, but it does work.

    First, the relevant types. Ranks and suits are enumerations, while cards are the obvious compound type (with a custom Show instance)

    import Text.ParserCombinators.Parsec
    
    data Suit = Clubs | Diamonds | Hearts | Spades deriving (Eq,Ord,Enum,Show)
    
    data Rank = Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten
              | Jack | Queen | King | Ace deriving (Eq,Ord,Enum,Show)  
    
    data Card = Card { rank :: Rank
                     , suit :: Suit } deriving (Eq,Ord,Bounded)
    
    instance Show Card where
        show (Card rank suit) = show rank ++ " of " ++ show suit
    

    Then we have the parsing code, which uses Parsec. You could develop this to be much more sophisticated, to return better error messages, etc.

    Note that, as Matvey said in the comments, the problem of parsing strings into their representations in the program is (or rather should be) orthogonal to how the enums are represented. Here I've cheated and broken the orthogonality: if you wanted to re-order the ranks (e.g. to have Ace rank below Two) then you would break the parsing code, because the parser depends on the internal representation of Two being 0, Three being 1 etc..

    A better approach would be to spell out all of the ranks in parseRank explicitly (which is what I do in the original code). I wrote it like this to (a) save some space, (b) illustrate how it's possible in principle to parse a number into a rank, and (c) give you an example of bad practice explicitly spelled out, so you can avoid it in the future.

    parseSuit :: Parser Suit
    parseSuit = do s <- oneOf "SDCH"
                   return $ case s of
                    'S' -> Spades
                    'D' -> Diamonds
                    'H' -> Hearts
                    'C' -> Clubs
    
    parseRank :: Parser Rank
    parseRank = do r <- oneOf "23456789TJQKA"
                   return $ case r of
                    'T' -> Ten
                    'J' -> Jack
                    'Q' -> Queen
                    'K' -> King
                    'A' -> Ace
                     n  -> toEnum (read [n] - 2)
    
    parseCard :: Parser Card
    parseCard = do r <- parseRank
                   s <- parseSuit
                   return $ Card { rank = r, suit = s }
    
    readCard :: String -> Either ParseError Card
    readCard str = parse parseCard "" str
    

    And here it is in action:

    *Cards> readCard "2C"
    Right Two of Clubs
    *Cards> readCard "JH"
    Right Jack of Hearts
    *Cards> readCard "AS"
    Right Ace of Spades
    

    Edit:

    @yatima2975 mentioned in the comments that you might be able to have some fun playing with OverloadedStrings. I haven't been able to get it to do much that's useful, but it seems promising. First you need to enable the language option by putting {-# LANGUAGE OverloadedStrings #-} at the top of your file, and include the line import GHC.Exts ( IsString(..) ) to import the relevant typeclass. Then you can make a Card into a string literal:

    instance IsString Card where
        fromString str = case readCard str of Right c -> c
    

    This allows you to pattern-match on the string representation of your card, rather than having to write out the types explicitly:

    isAce :: Card -> Bool
    isAce "AH" = True
    isAce "AC" = True
    isAce "AD" = True
    isAce "AS" = True
    isAce _    = False
    

    You can also use the string literals as input to functions:

    printAces = do
        let cards = ["2H", "JH", "AH"]
        mapM_ (\x -> putStrLn $ show x ++ ": " ++ show (isAce x)) cards
    

    And here it is in action:

    *Cards> printAces
    Two of Hearts: False
    Jack of Hearts: False
    Ace of Hearts: True