There's certain amount of boilerplate code in my game that keeps repeating time after time. I can't quite remove it, but I can hide it with template haskell.
newtype recap
I'll be using PlanetName as an example throughout the show. newtype is Haskell's way of defining a new type, that wraps around an old type. This lets us to give better meaning to the wrapped type. Instead of talking about Text, we can talk about PlanetName and we won't accidentally mix it up with StarName or ContentsOfAlexandrianLibrary. It comes with no performance cost at all, as the wrapping is removed during the compilation.
Below is how our PlanetName is defined:
newtype PlanetName
= MkPlanetName {_unPlanetName :: Text}
deriving (Show, Read, Eq)
It has:
type constructor PlanetName
data constructor MkPlanetName
single field _unPlanetName
type for that field Text
deriving clause, telling compiler to automatically generate Show, Read and Eq instances
If it were wrapping a Integer, we would add Ord and Num instances too.
These instances give us some basic functions that we can use to turn out value into String and back or compare two values to see if they're equal or not. Ord lets us compare their relative size and Num adds some basic arithmetics like addition and subtraction.
Remember, type constructor is used when talking about the type (function signatures, declaring type of a value, etc.), while data constructor is used to create values of the type ("Earth", "Mars", etc.). isPlanet :: PlanetName -> Bool states that isPlanet function takes one parameter of type PlanetName and returns value of type Bool. planet = MkPlanetName "Earth" creates a new value planet, that has type PlanetName and which value is MkPlanetName "Earth".
Boilerplate
When PlanetName is defined, I need to add some instances by hand: IsString, ToJSON, FromJSON, PersistField and PersistFieldSql.
IsString lets me use string literals in code, without having to call the data constructor. Compiler is smart enough to infer from context if string I typed should be PlanetName or something else.
ToJSON and FromJSON are used to turn value to and from json for transferring back and forth between client and server. In json our value is just simple string, but we still need to program that transformation.
PersistFieldSql tells Persistent (database layer I'm using) what type of database field should be created to hold this data in database.
PersistField contains functions for serializing our value to database and loading it from there.
Below is full code that I want to abstract out as much as I can:
newtype PlanetName
= MkPlanetName {_unPlanetName :: Text}
deriving (Show, Read, Eq)
instance IsString PlanetName where
fromString = (MkPlanetName . fromString)
instance ToJSON PlanetName where
toJSON = (toJSON . _unPlanetName)
instance FromJSON PlanetName where
parseJSON = (withText "PlanetName") (return . MkPlanetName)
instance PersistField PlanetName where
toPersistValue (MkPlanetName s) = PersistText s
fromPersistValue (PersistText s) = (Right $ MkPlanetName s)
fromPersistValue _ = Left "Failed to deserialize"
instance PersistFieldSql PlanetName where
sqlT