6

我想表示以下形式的类型:

(Card, Suit)

表示卡片游戏中的卡片,其中Card实例将在集合中:

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

并且Suit在集合中有实例:

{S, D, H, C}

如果不是为了数字,我会用两个 Data 声明来处理它:

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

但显然,将数字添加到那些 null arity 类型会失败。

所以我的问题是,如何模拟你在 C 中找到的那种枚举?

我想我误解了类型系统的基本观点,我们将不胜感激!

编辑:我将添加一些上下文:我想表示包含在这个欧拉问题中的数据,如您所见,数据以 1S 的形式表示黑桃 A,2D 表示菱形 2 等。 ..

我真正想要的是能够直接对字符串执行读取操作以获取相应的对象。

4

3 回答 3

22

实际上,当我开发扑克机器人时,我碰巧有一个方便的实现。它不是特别复杂,但确实有效。

一是相关类型。等级和花色是枚举,而卡片是明显的复合类型(带有自定义Show实例)

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

然后我们有解析代码,它使用 Parsec。您可以将其开发得更复杂,以返回更好的错误消息等。

请注意,正如 Matvey 在评论中所说,将字符串解析为其在程序中的表示的问题是(或者更确切地说应该是)正交于枚举的表示方式。在这里,我欺骗并破坏了正交性:如果您想重新排序排名(例如,Ace排名低于Two),那么您将破坏解析代码,因为解析器依赖于Twobeing 0Threebeing1等的内部表示。

更好的方法是明确拼出所有等级parseRank(这是我在原始代码中所做的)。我这样写是为了(a)节省一些空间,(b)说明原则上如何将数字解析为等级,以及(c)给你一个明确说明的不良做法的例子,这样你就可以避免它在将来。

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

它在行动中:

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

编辑:

@yatima2975 在评论中提到,你也许可以玩得开心OverloadedStrings。我无法让它做很多有用的事情,但它似乎很有希望。首先,您需要通过放在{-# LANGUAGE OverloadedStrings #-}文件顶部来启用语言选项,并包含import GHC.Exts ( IsString(..) )导入相关类型类的行。然后你可以把 aCard变成一个字符串文字:

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

这允许您对卡片的字符串表示进行模式匹配,而不必显式写出类型:

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

您还可以使用字符串文字作为函数的输入:

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

它在行动中:

*Cards> printAces
Two of Hearts: False
Jack of Hearts: False
Ace of Hearts: True
于 2012-04-25T12:45:27.080 回答
9
data Card = Two | Three | Four | Five | Six
          | Seven | Eight | Nine | Ten
          | Jack | Queen | King | Ace
    deriving Enum

实现Enum类型类意味着您可以使用fromEnumand在andtoEnum之间进行转换。CardInt

但是,如果这对您很重要fromEnum Two2您将不得不手动实现该Enum实例Card。(自动派生的实例从 开始0,就像 C 一样,但是如果不自己做这一切,就无法覆盖它。)

nb 您可能不需要Enum--- 如果您只想使用<和s 这样==的运算符Card,那么您需要使用deriving Ord.


编辑:

您不能使用readto 将String形式"2S""QH"a 转换为 a (Card, Suit),因为read希望字符串看起来像"(a,b)"(例如"(2,S)",以您最初要求的形式,或"(Two,S)"以我上面建议的形式)。

您必须自己编写一个函数来解析字符串。您可以使用解析器(例如 Parsec 或 Attoparsec),但在这种情况下,它应该足够简单,可以手动编写。

例如

{-# 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
于 2012-04-25T10:03:13.583 回答
2

我只是在数字前加上一个字母,或者更好的是,一个词。我也不会使用太多的单字母缩写 - <code>HK等是完全不可读的。

data Suit = Club | Spade | Heart | Diamond
data Card = Card1 | Card2 | … | Jack | Queen | King | Ace

......但我什至更喜欢戴夫的建议,即使用数字词(OneTwo)代替值。

于 2012-04-25T10:03:47.937 回答