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 e.g. 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?
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.
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, i.e.:
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With