Can type checking help me here? Perhaps with family types?

So, I have been writing this little football game for some time, and there is one thing that bothers me from the very beginning. The game follows the Yampa Arcade template , so there is 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 respond to messages, so there is a different type of sum:

 data Msg = BallMsg BM | PlayerMsg PM | GameMsg GM data BM = Gained | Lost data PM = GoTo Position | Shoot data GM = GoalScored | BallOutOfBounds 

The structure of Yampa is based on the so-called signaling functions. In our case, there are signal functions for playing ball, player and game. Roughly simplified:

 ballObj, playerObj, gameObj :: (Time -> (GameInput, [Msg])) -> (Time -> (ObjState, [(Id, Msg)])) 

So, for example, ballObj accepts a function that gives the GameInput (key strokes, game state, ...) and a list of messages specifically for the ball at any given time and returns a function that gives the state of the ball, and messages to other objects (ball, game , players) at any given time. In Yampa, a type signature actually looks a little nicer:

 ballObj, playerObj, gameObj :: SF (GameInput, [Msg]) (ObjState, [(Id, Msg)]) 

This formatted signature is important for the Yampa framework: (again, very roughly simplified), it creates a large signal function from the list 11 + 11 (players) + 1 (ball) + 1 (game) signal with functions of the same type (via dpSwitch) which is then launched (via reagent).

So, what bothers me: it makes sense to send BallMsg to Ball or PlayerMsg to a player. If someone ever sends, for example, GameMsg to Ball, the program will crash. Isn't there a way to get a type controller in position to avoid this? I recently read this nice Pokemon post about family types, and it looks like there is some analogy. Perhaps this could be the 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 it get me anywhere?

+7
source share
2 answers

At first glance, one serious problem stands out with your design: you combine completely different Ball , Player and Game objects under one type. If you need a union type over these objects, go the same way as with messages, making them separate types, that is:

 data AnyObject = AnyObjectBall Ball | AnyObjectPlayer Player | AnyObjectGame Game 

This way you can express both specific functions ( Ball -> BallMsg -> ... ) and general ones ( AnyObject -> AnyMsg -> ... ).

But if I understand your problem correctly, I think I have a solution for you that 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 
+1
source

When removing yampa arcade paper, it seems that you have a route function taken from their example.

My suggestion would be to change the route so that it does not accept a single list of objects, but instead one game object, one ball object and a collection of player objects. Then we have

 data BallMsg = ... data PlayerMsg = ... data GameMsg = ... data AnyMsg = ABallMsg BallMsg | APlayerMsg PlayerMsg | AGameMsg GameMsg 

Now route works with unified AnyMsg , but sends them to the right destination depending on their contents.

+2
source

All Articles