界面抽象设计

时间:2011-04-24 09:27:32

标签: haskell abstraction monads typeclass monad-transformers

目前,我尝试编写一个小型游戏程序(Skat)作为业余爱好项目。斯卡特是一个招人游戏,两名球员对阵一名球员。由于有不同类型的玩家(lokal播放器,网络播放器,计算机等),我想将界面抽象给玩家。

我的基本想法是使用类型类Player,它定义所有类型的事物,玩家必须做和知道(玩牌,获得有关谁赢得技巧的通知等)。然后,整个游戏只是由函数playSkat :: (Player a, Player b, Player c) => a -> b -> c -> IO ()完成,其中abc可能是不同类型的玩家。然后,玩家可以以实现定义的方式做出反应。 lokal播放器会在终端上收到一些消息,网络播放器可能会通过网络发送一些信息,而计算机播放器可能会计算出新策略。

因为玩家可能想要做一些IO并且肯定想要某种状态来跟踪私人事物,所以它必须存在于某种Monad中。所以我考虑像这样定义Player类:

class Player p where
  playCard :: [Card] -> p -> IO (Card,p)
  notifyFoo :: Event -> p -> IO p
  ...

这种模式似乎与状态转换器非常相似,但我不知道如何处理它。如果我把它作为额外的monad-transformer写在IO之上,那么我在一天结束时会有三个不同的monad。如何以良好的方式编写这种抽象?

为了澄清,我需要的是,通常的控制流程应该是这样的: 玩耍时,第一个玩家会玩牌,然后是第二个,最后是第三个。为此,逻辑需要为每个玩家执行函数playCard。之后,逻辑决定哪个玩家赢得了技巧并将获胜的信息发送给所有玩家。

4 个答案:

答案 0 :(得分:6)

首先,请记住,类型类的主要目的是允许重载函数,即您可以使用不同类型的单个函数。你真的不需要那样,所以你最好用

的记录类型
data Player = Player { playCard :: [Card] -> IO (Card, Player), ... }


其次,一些玩家需要IO的问题和一些不能用自定义monad解决的问题。我为TicTacToe游戏编写了相应的example code,这是我operational包的一部分。

答案 1 :(得分:4)

更好的设计是不要将IO作为任何播放器类型的一部分。 为什么玩家需要做IO?玩家可能需要获取信息并发送信息。创建一个反映它的界面。如果/当需要IO时,它将由playSkat执行。

如果你这样做,你可以拥有其他版本的playSkat,它们不会做任何IO,你也可以更容易地测试你的玩家,因为他们只通过类方法而不是通过IO进行交互。

答案 2 :(得分:1)

这就是我最终设计抽象的方式:

引擎可能想要的其中一个玩家的所有内容都编码在一个名为Message的大型GADT中,因为我并不总是需要答案。 GADT的参数是请求的返回值:

data Message answer where
  ReceiveHand :: [Card] -> Message ()
  RequestBid  :: Message (Maybe Int)
  HoldsBid    :: Int -> Message Bool
  ...

不同类型的玩家在一个类型类上被抽象,其中一个函数playerMessage允许引擎向玩家发送消息并请求答案。答案包含在Either中,因此如果无法返回答案,玩家可以返回相应的错误(例如,如果未执行该功能或网络处于打击状态等)。参数p是玩家存储私人数据和配置的状态记录。玩家是通过monad m抽象的,以允许一些玩家使用IO,而其他玩家则不需要它:

class Monad m => Player p m | p -> m where
  playerMessage :: Message answer -> p -> m (Either String answer,p)

修改

我问过another Question,因为我不满意一次又一次地输入上下文,所以我最终更改了代码来重新启用类型类Player。玩家没有自己的状态,但他们可以使用部分应用函数来模拟这个。有关详细信息,请参阅其他问题。

答案 3 :(得分:0)

一点都没想过,但也许还值得考虑。在这里,我注意到您在类型类函数中同时包含pp,我猜这意味着“更新”p。不知怎的,一个州的monad。

class (MonadIO m, MonadState p m) => Player p where
  playCard :: [Card] -> m Card
  notifyFoo :: Event -> m ()

同样,这只是一个自发的想法。我不保证它是明智的(甚至是可编辑的)。