Can the type checker help me out here? With type families, maybe?
So I'm writing this little soccer game for some time now, and there's one thing that bugs me from the very beginning. The game follows the Yampa Arcade pattern, so there's a sum type for the "objects" in the game:
data ObjState = Ball Id Pos Velo
| Player Id Team Number Pos Velo
| Game Id Score
Objects react to messages, so there's another sum type:
data Msg = BallMsg BM
| PlayerMsg PM
| GameMsg GM
data BM = Gained | Lost
data PM = GoTo Position | Shoot
data GM = GoalScored | BallOutOfBounds
The Yampa framework relies on so-called signal functions. In our case, there are signal functions for ball, player and game behaviour. Crudely simplified:
ballObj, playerObj, gameObj :: (Time -> (GameInput, [Msg]))
-> (Time -> (ObjState, [(Id, Msg)]))
So eg ballObj takes a function that yields the GameInput (key strokes, game state, ...) and a list of messages specifically for the ball at any given time, and returns a function that yields the ball's state and it's messages to other objects (ball, game, players) at any given time. In Yampa, the type signature actually looks a little nicer:
ballObj, playerObj, gameObj :: SF (GameInput, [Msg]) (ObjState, [(Id, Msg)])
This uniform type signature is important for the Yampa framework: (again, very crudely simplified) it builds a big signal function from a list of 11 + 11 (players) + 1 (ball) + 1 (game) signal functions with the same type (via dpSwitch) that it then runs (via reactimate).
So now, what bugs me: It only makes sense to send a BallMsg to a Ball, or a PlayerMsg to a Player. If ever someone sends for instance a GameMsg to a Ball, the program will crash. Isn't there a way to get the type checker in position to avoid this? I recently read this nice Pokemon post on type families, and it seems like there is some analogy. So maybe this might be a starting point:
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
(...)
Now, the SF function might look something like this:
forall b . (Receiver a, Receiver b) => SF (GameInput, [Msg a]) (a, [(b, Msg b)])
Will this get me anywhere?
Straight from the first glance one major problem with your design stands out: you unite completely different entities Ball
, Player
and Game
under a single type. If you need a union type over those entities, go the same way you have with the messages by making them separate types, ie:
data AnyObject = AnyObjectBall Ball
| AnyObjectPlayer Player
| AnyObjectGame Game
This way you'll be able to express both the specific functions ( Ball -> BallMsg -> ...
) and general ones ( AnyObject -> AnyMsg -> ...
).
But if I understand your problem correctly, I think I have a solution for you which does not require union types:
class Signal object message where
signal :: SF (GameInput, [message]) (object, [(Id, message)])
data Ball = Ball Id Pos Velo
data BallMsg = BallMsgGained | BallMsgLost
instance Signal Ball BallMsg where
-- ...
-- so on for Player and Game
Skimming the yampa arcade paper it seems like you have a route
function drawn from their example.
My suggestion would be you alter route
so it doesn't take a single list of objects, but instead a single game object, a single ball object, and a collection of player objects. Then have
data BallMsg = ...
data PlayerMsg = ...
data GameMsg = ...
data AnyMsg = ABallMsg BallMsg
| APlayerMsg PlayerMsg
| AGameMsg GameMsg
Now route
works on a uniform AnyMsg
but it dispatches them to the right destination depending on their contents.
上一篇: 区分滑动并点击Angular