所以我写这个小足球游戏已经有一段时间了,有一件事从一开始就让我烦恼。游戏遵循Yampa Arcade模式,因此游戏中的“对象”有一个 sum 类型:
data ObjState = Ball Id Pos Velo
| Player Id Team Number Pos Velo
| Game Id Score
对象对消息做出反应,因此还有另一种 sum 类型:
data Msg = BallMsg BM
| PlayerMsg PM
| GameMsg GM
data BM = Gained | Lost
data PM = GoTo Position | Shoot
data GM = GoalScored | BallOutOfBounds
Yampa 框架依赖于所谓的信号函数。在我们的例子中,有球、球员和比赛行为的信号函数。粗略简化:
ballObj, playerObj, gameObj :: (Time -> (GameInput, [Msg]))
-> (Time -> (ObjState, [(Id, Msg)]))
例如,ballObj 接受一个函数,该函数产生 GameInput(击键、游戏状态,...)和在任何给定时间专门针对球的消息列表,并返回一个函数,该函数产生球的状态和消息给其他对象(球、比赛、球员)在任何给定时间。在 Yampa 中,类型签名实际上看起来更好一些:
ballObj, playerObj, gameObj :: SF (GameInput, [Msg]) (ObjState, [(Id, Msg)])
这种统一的类型签名对于 Yampa 框架很重要:(再次,非常粗略地简化)它从具有相同类型的 11 + 11(玩家)+1(球)+1(游戏)信号函数的列表中构建一个大信号函数(通过 dpSwitch)然后运行(通过反应)。
所以现在,让我烦恼的是:将 BallMsg 发送给 Ball 或 PlayerMsg 发送给 Player 才有意义。例如,如果有人向 Ball 发送 GameMsg,程序就会崩溃。有没有办法让类型检查器就位以避免这种情况?我最近读了一篇关于类型族的漂亮口袋妖怪帖子,似乎有一些类比。所以也许这可能是一个起点:
class Receiver a where
Msg a :: *
putAddress :: Msg a -> a -> Msg a
data BallObj = ...
data GameObj = ...
data PlayerObj = ...
instance Receiver BallObj where
Msg BallObj = Gained | Lost
(...)
现在,SF 函数可能看起来像这样:
forall b . (Receiver a, Receiver b) => SF (GameInput, [Msg a]) (a, [(b, Msg b)])
这会让我到任何地方吗?