We put type classes to use all over the core and contrib libraries in PureScript, from monad transformers to profunctor optics, and much more. But for all the advanced abstractions we have available, the best uses of type classes can be the simplest ones.
Ord type class, which gives us more options and allows us to use our own custom domain-specific data types as keys.
We might want to track people by either their email address or phone number, for example. In that case, it would make the most sense to use a sum type as the key in a dictionary:
import Data.Map (Map) data PersonKey = ByEmail EmailAddress | ByPhone PhoneNumber data PersonValue = Naughty | Nice type Database = Map PersonKey PersonValue
In order to use the operations from
Data.Map though, we need an instance for
Ord PersonKey (and also the
Eq superclass instance). We could write one by hand:
instance eqPersonKey :: Eq PersonKey where eq (ByEmail a) (ByEmail b) = a == b eq (ByPhone a) (ByPhone b) = a == b eq _ _ = false instance ordPersonKey :: Ord PersonKey where compare (ByEmail a) (ByEmail b) = compare a b compare (ByEmail _) _ = LT compare (ByPhone a) (ByPhone b) = compare a b compare (ByPhone _) _ = GT
But as we can see, these instances are mostly boilerplate, simply following the shapes of the types. Also, it's easy to make mistakes, particularly when writing
Ord instances for sum types.
These sorts of instances are so common that the PureScript compiler can just write them for us! And for problems like these, where the code so closely follows the type structure, it makes sense to make the compiler do the work for us.
Simply omit the body of the instance declaration, and add the
derive keyword at the front, as follows:
derive instance eqPersonKey :: Eq PersonKey derive instance ordPersonKey :: Ord PersonKey
and we get valid
Ord instances for free! Now we can use all of the
Data.Set APIs, and plenty of other generic structures and algorithms with our custom types.
Deriving these instances works for all data types, including those which contain records, as long as the types contained in the fields are themselves instances of
Ord. This makes
derive statements particularly useful for generating code involving custom data types.
We can even derive instances when our data type definitions contain type arguments. We just have to make sure to add constraints for the appropriate instances in the context. For example, to derive instances for
Maybe, we could write:
derive instance eqMaybe :: Eq a => Eq (Maybe a) derive instance ordMaybe :: Ord a => Ord (Maybe a)
This obviously doesn't work for every type class, but the compiler does have built-in support for a few other useful type classes. We'll cover these later this month, and see how this sort of code generation can really help cut down the amount of boilerplate code in our applications.