Haskell 界面抽象设计
目前,我试图写一个小游戏程序(Skat)作为一个爱好项目。Skat是一种两人对一人的把戏游戏。由于有不同类型的播放器(lokal播放器、网络播放器、计算机等),我想将接口抽象到播放器 我的基本想法是使用一个typeclassHaskell 界面抽象设计,haskell,abstraction,monads,typeclass,monad-transformers,Haskell,Abstraction,Monads,Typeclass,Monad Transformers,目前,我试图写一个小游戏程序(Skat)作为一个爱好项目。Skat是一种两人对一人的把戏游戏。由于有不同类型的播放器(lokal播放器、网络播放器、计算机等),我想将接口抽象到播放器 我的基本想法是使用一个typeclassPlayer,它定义了玩家必须做和知道的所有事情(玩一张牌,得到关于谁赢了这个把戏的通知,等等)。然后,整个游戏就是通过一个函数来完成的。playSkat::(玩家a,玩家b,玩家c)=>a->b->c->IO(),其中a,b和c可能是不同类型的玩家。然后,玩家可能会以实现定
Player
,它定义了玩家必须做和知道的所有事情(玩一张牌,得到关于谁赢了这个把戏的通知,等等)。然后,整个游戏就是通过一个函数来完成的。playSkat::(玩家a,玩家b,玩家c)=>a->b->c->IO()
,其中a
,b
和c
可能是不同类型的玩家。然后,玩家可能会以实现定义的方式做出反应。lokal玩家会在他的终端上收到一些信息,网络玩家可能会通过网络发送一些信息,计算机玩家可能会计算出一个新的策略
因为玩家可能想要做一些IO,并且肯定想要有某种状态来跟踪私人事物,所以它必须生活在某种Monad中。因此,我考虑像这样定义Player
类:
class Player p where
playCard :: [Card] -> p -> IO (Card,p)
notifyFoo :: Event -> p -> IO p
...
这种模式似乎非常类似于状态转换器,但我不知道舒尔如何处理它。如果我把它作为IO上的一个额外的单声道转换器来写的话,我在一天结束时有三个不同的单声道。我怎样才能以一种好的方式写出这个抽象
为了澄清,我需要的是,通常的控制流应该是什么样子:玩把戏时,第一个玩家先玩一张牌,然后是第二张牌,最后是第三张牌。为此,逻辑需要为每个玩家执行功能
playCard
trice。然后,逻辑决定,哪个玩家赢了把戏,并将谁赢的信息发送给所有玩家。根本没有考虑过这一点,但可能仍然值得考虑。在这里,我注意到类型类函数中既有p
in,也有p
out,我猜这意味着那些“更新”p
。某种程度上是一种状态单子
class (MonadIO m, MonadState p m) => Player p where
playCard :: [Card] -> m Card
notifyFoo :: Event -> m ()
同样,这只是一个自发的想法。我不能保证它是明智的(甚至是可编译的)。更好的设计是不将IO作为任何播放器类型的一部分。 为什么玩家需要执行IO?玩家可能需要获取信息并发送信息。制作一个反映这一点的界面。如果需要IO,将由playSkat执行
如果你这样做了,你就可以让其他版本的playSkat不做任何IO,你也可以更轻松地测试你的播放器,因为它们只通过类方法而不是IO进行交互。首先,请记住类型类的主要目的是允许函数重载,也就是说,您可以在不同类型上使用单个函数。您实际上并不需要这样做,因此您最好使用符合以下要求的记录类型:
data Player = Player { playCard :: [Card] -> IO (Card, Player), ... }
第二,有些玩家需要IO,有些玩家不需要IO的问题可以通过自定义单子解决。我已经为Tictatoe的一个游戏编写了相应的代码,这是我的软件包的一部分。这就是我最终设计抽象的方式: 引擎可能希望从其中一个玩家那里得到的所有东西都被编码在一个名为
Message
的大GADT中,因为我并不总是需要答案。GADT的参数是请求的返回值:
data Message answer where
ReceiveHand :: [Card] -> Message ()
RequestBid :: Message (Maybe Int)
HoldsBid :: Int -> Message Bool
...
不同类型的玩家通过一个类型类进行抽象,该类具有一个函数playerMessage
,该函数允许引擎向玩家发送消息并请求回答。答案被包装在或中,因此如果无法返回答案,玩家可以返回相应的错误(例如,如果功能未实现或网络罢工等)。参数p
是播放器存储私有数据和配置的状态记录。播放器在monadm
上进行抽象,以允许某些播放器使用IO,而其他播放器则不需要IO:
class Monad m => Player p m | p -> m where
playerMessage :: Message answer -> p -> m (Either String answer,p)
编辑
我问,因为我不喜欢一次又一次地输入上下文,所以我最终更改了代码以具体化typeclassPlayer
。玩家自己没有状态,但他们可以使用部分应用函数来模拟。有关详细信息,请参见其他问题。Hm。。。这个想法是使用类Player
来抽象玩家的行为。但显然,大多数类型的玩家都需要进行IO。考虑人类玩家:计算机必须输出某种提示来请求进一步的输入。对于远程播放机,计算机必须执行一些IO操作,以便通过网络发送所需信息的请求。唯一可能不需要执行IO的播放器是计算机播放器。我希望play
功能就是引擎。您可以将Player
类看作是一种前端。我认为只有在该接口“内部”有另一个纯接口时,才可以在该接口中使用IO。一旦你进入IO测试就变得更加困难了。为此,我可以让底层的monad成为typeclass的另一个参数。然后我就可以不用IO进行测试了。但这只对特定类型的玩家重要,因为他们中的大多数都必须执行IO。这也是我的想法。但我的问题是,如何同时处理三个不同的状态单子?正如我所悲伤的,一个播放器并不总是相同的数据类型。有时它是一台计算机,有时是用户的界面,或者是完全不同的东西。我希望每种球员都有不同的表现。您展示的基本上是刚刚作为数据类型写入的类型classPlayer
。实际上,我建议您将类型classPlayer
作为数据类型写入。此外,请看第二部分,Joost Visser及其同事撰写的《卡米拉复兴:VDM遇见哈斯克尔》一文中可能有一些想法值得挖掘。对一元s有不同的解释