【问题标题】:Haskell way to go about enumsHaskell 处理枚举的方法
【发布时间】:2019-11-30 17:04:08
【问题描述】:

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

(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 中找到的那种枚举?

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

编辑:我将添加一些上下文:我想表示包含在 this Euler problem 中的数据,如您所见,数据以 1S 的形式表示黑桃 A,2D 表示菱形 2等等……

我真正想要的是能够直接对字符串进行读操作,得到对应的对象。

【问题讨论】:

  • 将诸如“1S”或“2D”之类的字符串解析为卡片的能力实际上与枚举表示是正交的。
  • @MatveyAksenov 好吧,我明白这一点。我一直在寻找的是在类型系统中最惯用的方法。无论如何,我想我问错了问题。

标签: haskell types enumeration


【解决方案1】:

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

首先,相关类型。等级和花色是枚举,而卡片是明显的复合类型(带有自定义 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 在 cmets 中所说,将字符串解析为程序中的表示的问题是(或者更确切地说 应该)与枚举的表示方式正交。在这里,我欺骗并破坏了正交性:如果您想重新排序排名(例如,让Ace 排名低于Two),那么您将破坏解析代码,因为解析器依赖于 @ 的内部表示987654325@是0,Three是1等等。

更好的方法是明确拼出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 在 cmets 中提到,您也许可以在玩 OverloadedStrings 时获得一些乐趣。我无法让它做很多有用的事情,但它似乎很有希望。首先,您需要通过将{-# LANGUAGE OverloadedStrings #-} 放在文件顶部来启用语言选项,并包含import GHC.Exts ( IsString(..) ) 行以导入相关的类型类。然后你可以将Card 变成字符串文字:

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

【讨论】:

  • 感谢您的回答。快速提问:让您的 Card 类型成为 Read 的实例不是更好吗?我还没有使用 parsec,所以我不知道忽略 Read 并将其解析到一边是否常见。
  • 您可以添加 Read 的派生实例,尽管您只能获得解析字符串的能力,例如“Card { rank = Two, suit = Clubs }”,这并没有那么有用。您可以自己指定一个 Read 实例,这基本上归结为为您的类型编写一个解析器。我认为没有办法比我在这里少写几行。
  • 现在使用OverloadedStringLiterals有哪些选项?这会让你非常接近 DSL!
  • @yatima2975 我进行了编辑以包含有关字符串文字的一些信息。如果您对该语言扩展有更多经验,也许您可​​以充实它?
  • 嗯,这个扩展似乎是我一直在寻找的,最后。感谢@yatima2975 提出这个问题:)
【解决方案2】:
data Card = Two | Three | Four | Five | Six
          | Seven | Eight | Nine | Ten
          | Jack | Queen | King | Ace
    deriving Enum

实现Enum 类型类意味着您可以使用fromEnum 和toEnum 在Card 和Int 之间进行转换。

但是,如果 fromEnum Two 是 2 对您很重要,则您必须手动为 Card 实现 Enum 实例。 (自动派生的实例从 0 开始,就像 C 一样,但如果不自己完成所有操作,就无法覆盖它。)

n.b.您可能不需要Enum --- 如果您只想在Cards 中使用&lt; 和== 等运算符,那么您需要使用deriving Ord。


编辑:

您不能使用read 将"2S" 或"QH" 形式的String 转换为(Card, Suit),因为read 会期望字符串看起来像"(a,b)"(例如"(2,S)" in您最初要求的表格,或我上面建议的表格中的"(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

【讨论】:

    【解决方案3】:

    我只是在数字前加上一个字母,或者更好的是,一个词。我也不会使用太多的单字母缩写——H、K 等完全不可读。

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-06-26
      • 1970-01-01
      • 2010-10-11
      • 2011-10-14
      • 2017-09-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多