我已经完成了扑克游戏的编写工作,现在我试图展示不同的卡片绘制算法实现,每一轮都可以进行交换。我写了一个类,它定义了类需要的两个函数实例,以及这个类的几个实例:
class Drawable a where
initDeck :: IO a
draw :: a -> IO (ShuffleType, Card)
newtype Deck = Deck [Card]
newtype RandomIndex = RandomIndex Deck
newtype KnuthShuffle = KnuthShuffle Deck
instance Drawable RandomIndex where
initDeck = return $ coerce fullDeck
draw x = do
let cards = coerce x
randomNum <- getStdRandom $ randomR (0, length cards- 1)
let (beginning, card:end) = splitAt randomNum cards
return (IsRandomIndex . coerce $ beginning ++ end, card)
instance Drawable KnuthShuffle where
initDeck = coerce $ shuffle (length fullDeck - 1) fullDeck
where shuffle 0 xs = return xs
shuffle i xs = do
j <- getStdRandom $ randomR (0, i)
shuffle (i-1) (swap i j xs)
draw x = return (IsKnuthShuffle $ coerce deck, card)
where (card:deck) = coerce x
-- adapted from https://stackoverflow.com/a/30551130/8737306
swap :: Int -> Int -> [a] -> [a]
swap i j xs
| i == j = xs
| otherwise = let elemI = xs !! i
elemJ = xs !! j
left = take j xs
middle = take (i - j - 1) (drop (j + 1) xs)
right = drop (i + 1) xs
in left ++ [elemI] ++ middle ++ [elemJ] ++ right
Coerce只是为了方便打开多个新类型而使用,我使用它来防止模块外的调用者搞乱内部卡片并确保自上次调用绘制以来套牌状态是相同的。
现在,我希望能够在游戏中交换使用它们。我必须以某种方式从initDeck存储返回的类型或绘制我的游戏数据类型,以允许我再次调用draw。它也必须是多态的,因此它可以存储RandomIndex或KnuthShuffle。
我最初尝试过这种数据类型的参数化,所以我有类似
的东西data Game a = Game {
players :: [Player],
pots :: [Pot],
cards :: a
}
然后,我可以有一个功能dealCard:
dealCard :: (Drawable a) => Game a -> Game a
dealCard game = do
deck <- initDeck
(card, newDeck) <- draw deck
putStrLn $ "You drew the " ++ show card
cards .= newDeck -- I'm using lenses and stateT
但是现在它还不知道要使用什么实例。所以我们告诉它:
deck <- initDeck :: IO KnuthShuffle
不幸的是,现在我们为deck提供了一个显式类型,它不再仅仅是一个Drawable a,因此我们有一个类型不匹配。
我的第二个想法是制作复合数据类型:
data ShuffleType = IsKnuthShuffle KnuthShuffle
| IsRandomIndex RandomIndex
然后我们可以从draw返回一个ShuffleType,因为我们知道它在每个实现中的类型。但是,我们无法将此返回的类型传回画面,因为我们无法解开它。
现在我已经陷入困境,最终我希望能够做到这样的事情。
nextRound :: Game -> String -> Game
nextRound game newShuffle = do
case newShuffle of
"knuth" -> do
deck <- initDeck :: IO KnuthShuffle
cards .= deck
"randomIndex" -> do
deck <- initDeck :: IO RandomIndex
cards .= deck
然后,在我的交易卡功能中,只需调用draw就会自动选择正确的表示形式。
我觉得我以错误的方式解决这个问题,我通常能够很好地完成Haskell的所有工作,但现在我不确定如何实现这一点。 我甚至不确定一个类是否是最好的方法,而且甚至可能更好的是有一个initKnuth,drawKnuth,initRandomIndex等,但我希望避免这种事情。
我试图简化这个数量,但如果需要,我可以提供更深入的实际代码。
编辑:感谢@ DanielWagner的建议,我放弃了课堂理念并提供了明确的实例。代码几乎保持不变,我现在可以实现我想要的功能:
drawCard :: GameStateT Card
drawCard = do
s <- get
shuffle <- lift $ readIORef (s^.shuffleType)
let oldDeck = s^.cardInfo.deck
case shuffle of
Knuth -> do
let (card, newDeck) = drawKnuth oldDeck
cardInfo.deck .= IsKnuth newDeck
cardInfo.tableCards %= (++ [card])
return card
RandomIndex -> do
(card, newDeck) <- lift $ drawRandomIndex oldDeck
cardInfo.deck .= IsRandomIndex newDeck
cardInfo.tableCards %= (++ [card])
return card
这样做的缺点是,如果我打算实现其中的一些,那么它有点复制糊涂,但我想这可以通过分配卡片来解决,newDeck是if语句的结果。
答案 0 :(得分:4)
我将总结评论中讨论的内容。你真的不需要一个类型类。让我们说我们想要两种不同的牌组:Knuth和随机。
data KnuthShuffle = ...
data RandomIndex = ...
然后让我们定义一个类型,可以是其中之一,就像你建议做的那样。
data ShuffleType = IsKnuthShuffle KnuthShuffle
| IsRandomIndex RandomIndex
然后我们将所有操作都放在ShuffleType
上。
initDeckKnuth :: IO KnuthShuffle
initDeckKnuth = ...
initDeckRandom :: IO RandomIndex
initDeckRandom = ...
如果我们有KnuthShuffle
或RandomIndex
并希望将其转换为统一ShuffleType
,我们会应用相应的构造函数。
nextRound :: Game -> String -> Game
nextRound game newShuffle = do
case newShuffle of
"knuth" -> do
deck <- IsKnuthShuffle initDeckKnuth
cards .= deck
"randomIndex" -> do
deck <- IsRandomIndex initDeckRandom
cards .= deck
然后我们需要做的就是能够从中抽出卡片。
draw :: ShuffleType -> IO (ShuffleType, Card)
draw (IsKnuthShuffle x) = ... -- The KnuthShuffle case here
draw (IsRandomIndex x) = ... -- The RandomIndex case here