In the meeting, we had some questions about how some of the type class instance declarations worked. I've looked into it and now have some explanations:
data Message = Ping ProcessId | Pong ProcessId
deriving (Typeable, Generic)
instance Binary Message
We wondered, how is it that we can declare an instance of Binary for Message, without any code? Surely there are some methods to be filled in.
It turns out that yes, Binary has methods put and get that need to be defined. But it has default definitions for those methods. Those default definitions rely on additional type constraints, a feature enabled by the DefaultSignatures extension.
default put :: (Generic t, GBinaryPut (Rep t)) => t -> Put
put = gput . from
default get :: (Generic t, GBinaryGet (Rep t)) => Get t
get = to `fmap` gget
So, as long as you have an instance Generic Message (which we have derived) and instances of GBinaryPut (Rep Message)
and GBinaryGet (Rep Message)
, then you will get those methods for free.
We get Rep Message from the Typeable instance - it's an associated type family. There are Binary instances for base types defined in Data.Binary.Class. And there are instances of Binary in in Data.Binary.Generic which work on type reps for custom types, and rely on the instances for the base types.
As for the other mysterious instance:
class (Binary a, Typeable a) => Serializable a
instance (Binary a, Typeable a) => Serializable a
Turns out that's not so mysterious, and basically does what it looks like. When a function is defined with Serializable as a constraint, it will resolve to that instance, and then check to see that the constraints are satisfied - if not, it will give an error.
Why are there no methods for this class? Instead, there are standalone functions with the Serializable constraint, that make use of the methods of Binary and Typeable. As long as the Serializable constraint is satisfied, then we know the constraints for Binary and Typeable must be satisfied, too, so that works.
It could have alternately been defined with Serializable being a constraint kind:
type Serializable a = (Binary a, Typeable a)
The difference there is that it wouldn't be possible to define custom instances of Serializable. You can't make an instance for something that's not a class.