动态地在Haskell类的实现之间交换

时间:2017-10-31 21:39:51

标签: haskell

我已经完成了扑克游戏的编写工作,现在我试图展示不同的卡片绘制算法实现,每一轮都可以进行交换。我写了一个类,它定义了类需要的两个函数实例,以及这个类的几个实例:

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语句的结果。

1 个答案:

答案 0 :(得分:4)

我将总结评论中讨论的内容。你真的不需要一个类型类。让我们说我们想要两种不同的牌组:Knuth和随机。

data KnuthShuffle = ...
data RandomIndex  = ...

然后让我们定义一个类型,可以是其中之一,就像你建议做的那样。

data ShuffleType = IsKnuthShuffle KnuthShuffle
                 | IsRandomIndex  RandomIndex

然后我们将所有操作都放在ShuffleType上。

initDeckKnuth :: IO KnuthShuffle
initDeckKnuth = ...

initDeckRandom :: IO RandomIndex
initDeckRandom = ...

如果我们有KnuthShuffleRandomIndex并希望将其转换为统一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