Template Haskell
(3 examples of how we're using it)
When to use template haskell?
- Newtypes
- Boilerplate functions
- Boilerplate instances
- When you have a class of types that all need to satisfy the same feature set
How does it work?
- Q monad
- Can use fail, error, have a match error, whatever!
- Can use QuasiQuoters
- Constructors
- Upper case constructors are pure values
- Lower case smart constructors are effects in Q
- Constructors are suffixed with type of meta-type
- e.g. InstanceD/instanceD are type Dec/Q Dec respectively
- You're writing a "splice" that gets stitched in to the source code and compiled wherever it's called
How does it work?
-- TH.hs
fooQ :: Name -> Q [Dec]
fooQ name = ...
-- Types.hs
data MyName = MyName
fooQ ''MyName
-- Spec.hs
data MyThing = MyThing
fooQ ''MyThing
spec :: Spec
spec = it "if it compiles, it worked!"- TH splices must be defined in a separate module than the module in which they are invoked
- It's compiled n + 1 times:
- Once in the Q monad
- Each time it's invoked by stitching in necessary values
- It's compiled n + 1 times:
- Test the splice with compile-time checks
Example 1: Opaleye
-- lots of boilerplate
newtype MyType { unMyType :: Text }
newtype PGMyType = PGMyType { unPGMyType :: PGText }
deriving (PGString)
makeWrapped ''MyType
instance Default Constant MyType (Column PGMyType) where
def = dimap (view _Wrapped) (unsafeCoerceColumn :: Column PGText -> Column PGMyType) def
instance FromField MyType where
fromField f bs = view _Unwrapped <$> fromField f bs
instance QueryRunnerColumnDefault PGMyType MyType where
queryRunnerColumnDefault = fieldQueryRunnerColumnExample 1: Opaleye
makeOpaleyeTextNewtypeInstances :: Name -> Q [Dec]
makeOpaleyeTextNewtypeInstances tyName =
makeOpaleyeNewtypeInstances tyName ''PGText [''PGString]
makeOpaleyeNewtypeInstances :: Name -> Name -> [Name] -> Q [Dec]
makeOpaleyeNewtypeInstances tyName targetName derivingNames = do
let pgName = prefixName "PG" tyName
map mconcat . sequence $
[ makePGType tyName targetName derivingNames
, [d| -- this is a QuasiQuoter
instance Default Constant $(TH.conT tyName) (Column $(TH.conT pgName)) where
def = dimap
(view _Wrapped)
(unsafeCoerceColumn :: Column $(TH.conT targetName) -> Column $(TH.conT pgName))
def
|]
, [d|
instance FromField $(TH.conT tyName) where
fromField f bs = view _Unwrapped <$> fromField f bs
|]
, makeQueryRunnerColumnDefault tyName pgName ]
- QuasiQuoters
Example 1: Opaleye
-- lots of boilerplate
newtype MyType { unMyType :: Text }
newtype PGMyType = PGMyType { unPGMyType :: PGText }
deriving (PGString)
makeWrapped ''MyType
instance Default Constant MyType (Column PGMyType) where
def = dimap (view _Wrapped) (unsafeCoerceColumn :: Column PGText -> Column PGMyType) def
instance FromField MyType where
fromField f bs = view _Unwrapped <$> fromField f bs
instance QueryRunnerColumnDefault PGMyType MyType where
queryRunnerColumnDefault = fieldQueryRunnerColumnnewtype MyType { unMyType :: Text }
makeWrapped ''MyType
makeOpaleyeTextNewtypeInstances ''MyTypeExample 2: Lens
-- lots of boilerplate
data Foo' bar baz bin = Foo
{ _fooBar :: bar
, _fooBaz :: baz
, _fooBin :: bin
} deriving (Eq, Ord, Show)
type Foo = Foo' () Int Char
makeLenses ''Foo'
class HasFoo r
where foo :: Lens' r Foo
instance HasFoo Foo where
where foo = id
(Actually, it's Opaleye again)
Example 2: Lens
makeClassyAlias :: Name -> DecsQ
makeClassyAlias tySyn = do
let extractTyName = \ case
AppT (ConT tyName) _ -> pure tyName
AppT x _ -> extractTyName x
syn <- reify tySyn >>= \ case
TyConI (TySynD _ _ s@(AppT _ _)) -> pure s
tyName <- extractTyName syn
let rName = mkName "r"
tySynName = nameBase tySyn
(className, methodName) <- case tySynName of
x:xs -> pure (mkName $ "Has" <> tySynName, mkName (charToLower x:xs))
let alias = ...
mconcat <$> sequence [ makeLenses tyName, alias ]
(Actually, it's Opaleye again)
- Using pure constructors (upper case) with reify
Example 2: Lens
(Actually, it's Opaleye again)
-- lots of boilerplate
data Foo' bar baz bin = Foo
{ _fooBar :: bar
, _fooBaz :: baz
, _fooBin :: bin
} deriving (Eq, Ord, Show)
type Foo = Foo' () Int Char
makeLenses ''Foo'
class HasFoo r
where foo :: Lens' r Foo
instance HasFoo Foo where
where foo = id
data Foo' bar baz bin = Foo
{ _fooBar :: bar
, _fooBaz :: baz
, _fooBin :: bin
} deriving (Eq, Ord, Show)
type Foo = Foo' () Int Char
makeClassyAlias ''FooExample 3: Enumerations
-- lots of boilerplate
data MyEnum = MyEnumFoo | MyEnumBar
instance GEnum MyEnum where
genum = genumDefault
_MyEnum :: Prism' Text MyEnum
_MyEnum = enumPrismWithPrefix "MyEnum"
instance FromJSON MyEnum where
parseJSON = enumFromJsonWithPrefix "MyEnum"
instance ToJSON MyEnum where
toJSON = enumToJsonWithPrefix "MyEnum"
instance ToJSON MyEnum where
toField = encodeUtf8 . review _MyEnum -- will encode as camel case(Hint: still Opaleye)
Example 3: Enumerations
makeEnumInstancesUsing :: Q Exp -> Q Exp -> Q Exp -> Name -> Q [Dec]
makeEnumInstancesUsing prismExp toJsonExp fromJsonExp tyName = do
let base = nameBase tyName
prismName <- newName $ "_" <> base
-- 50 more lines
-- ooo000o0o000oo00oooh camelCase
makeEnumInstances :: String -> Name -> Q [Dec]
makeEnumInstances prefix tyName =
let base = nameBase tyName
prismExp = [| enumPrismWithPrefix prefix |]
toJsonExp = [| enumToJsonWithPrefix prefix |]
fromJsonExp = [| enumFromJsonWithPrefix prefix base |]
in makeEnumInstancesUsing prismExp toJsonExp fromJsonExp tyName
-- ooo00o0o0o0ooo0oh snake_case
makeSnakeEnumInstances :: String -> Name -> Q [Dec]
makeSnakeEnumInstances prefix tyName =
let base = nameBase tyName
prismExp = [| snakeEnumPrismWithPrefix prefix |]
toJsonExp = [| snakeEnumToJsonWithPrefix prefix |]
fromJsonExp = [| snakeEnumFromJsonWithPrefix prefix base |]
in makeEnumInstancesUsing prismExp toJsonExp fromJsonExp tyName(Hint: still Opaleye)
- More QuasiQuoters
Example 3: Enumerations
data MyEnum = MyEnumFoo | MyEnumBar
makeEnumInstances "MyEnum" ''MyEnum(Hint: still Opaleye)
-- lots of boilerplate
data MyEnum = MyEnumFoo | MyEnumBar
instance GEnum MyEnum where
genum = genumDefault
_MyEnum :: Prism' Text MyEnum
_MyEnum = enumPrismWithPrefix "MyEnum"
instance FromJSON MyEnum where
parseJSON = enumFromJsonWithPrefix "MyEnum"
instance ToJSON MyEnum where
toJSON = enumToJsonWithPrefix "MyEnum"
instance ToJSON MyEnum where
toField = encodeUtf8 . review _MyEnum -- will encode as camel caseWhen to use template haskell?
Opaleye- Newtypes
- Boilerplate functions
- Boilerplate instances
- When you have a class of types that all need to satisfy the same feature set
Template Haskell
By dfithian
Template Haskell
- 363