Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 664
m09 Avatar asked Apr 25 '12 09:04

m09


People also ask

What is an idiomatic way of representing enums in go?

It's true that the above examples of using const and iota are the most idiomatic ways of representing primitive enums in Go.

Should enums be stored in database?

By keeping the enum in your database, and adding a foreign key on the table that contains an enum value you ensure that no code ever enters incorrect values for that column. This helps your data integrity and is the most obvious reason IMO you should have tables for enums.

Where are enums stored?

-Dynamic enums are stored in java classes. -Static enums are stored in database.

Can you add methods to enums?

An enum class can include methods and fields just like regular classes. When we create an enum class, the compiler will create instances (objects) of each enum constants. Also, all enum constant is always public static final by default.


2 Answers

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
like image 136
Chris Taylor Avatar answered Sep 18 '22 23:09

Chris Taylor


data Card = Two | Three | Four | Five | Six
          | Seven | Eight | Nine | Ten
          | Jack | Queen | King | Ace
    deriving Enum

Implementing the Enum typeclass means you can use fromEnum and toEnum to convert between Card and Int.

However, if it's important to you that fromEnum Two is 2, you will have to implement the Enum instance for Card by hand. (The autoderived instance starts at 0, just like C, but there's no way of overriding that without doing it all yourself.)

n.b. You might not need Enum --- if all you want is to use operators like < and == with your Cards, then you need to use deriving Ord.


Edit:

You cannot use read to turn a String of the form "2S" or "QH" into a (Card, Suit) because read will expect the string to look like "(a,b)" (e.g. "(2,S)" in the form you initially asked for, or "(Two,S)" in the form I suggested above).

You will have to write a function to parse the string yourself. You could use a parser (e.g. Parsec or Attoparsec), but in this case it should be simple enough to write by hand.

e.g.

{-# LANGUAGE TupleSections #-}

parseSuit :: String -> Maybe Suit
parseSuit "S" = Just S
...
parseSuit _   = Nothing

parseCard :: String -> Maybe (Card, Suit)
parseCard ('2' : s) = fmap (Two,) (parseSuit s)
...
parseCard _         = Nothing
like image 34
dave4420 Avatar answered Sep 20 '22 23:09

dave4420