{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} module DeriveHasField ( module GHC.Records, deriveHasFieldWith, ) where import Control.Monad import Data.Char (toLower) import Data.Foldable as Foldable import Data.Traversable (for) import GHC.Records import Language.Haskell.TH import Language.Haskell.TH.Datatype deriveHasFieldWith :: (String -> String) -> Name -> DecsQ deriveHasFieldWith fieldModifier = makeDeriveHasField fieldModifier <=< reifyDatatype makeDeriveHasField :: (String -> String) -> DatatypeInfo -> DecsQ makeDeriveHasField fieldModifier datatypeInfo = do -- We do not support sum of product types constructorInfo <- case datatypeInfo.datatypeCons of [info] -> pure info _ -> fail "deriveHasField: only supports product types with a single data constructor" -- We only support data and newtype declarations when (datatypeInfo.datatypeVariant `Foldable.notElem` [Datatype, Newtype]) $ fail "deriveHasField: only supports data and newtype" -- We only support data types with field names and concrete types let isConcreteType = \case ConT _ -> True AppT _ _ -> True _ -> False recordConstructorNames <- case constructorInfo.constructorVariant of RecordConstructor names -> pure names _ -> fail "deriveHasField: only supports constructors with field names" unless (Foldable.all isConcreteType constructorInfo.constructorFields) $ fail "deriveHasField: only supports concrete field types" -- Build the instances let constructorNamesAndTypes :: [(Name, Type)] constructorNamesAndTypes = zip recordConstructorNames constructorInfo.constructorFields parentType = foldl' (\acc var -> appT acc (varT $ tyVarBndrToName var)) (conT datatypeInfo.datatypeName) datatypeInfo.datatypeVars decs <- for constructorNamesAndTypes $ \(name, ty) -> let currentFieldName = nameBase name wantedFieldName = lowerFirst $ fieldModifier currentFieldName litTCurrentField = litT $ strTyLit currentFieldName litTFieldWanted = litT $ strTyLit wantedFieldName in if currentFieldName == wantedFieldName then fail "deriveHasField: after applying fieldModifier, field didn't change" else [d| instance HasField $litTFieldWanted $parentType $(pure ty) where getField = $(appTypeE (varE $ mkName "getField") litTCurrentField) |] pure $ Foldable.concat decs lowerFirst :: String -> String lowerFirst = \case [] -> [] (x : xs) -> toLower x : xs tyVarBndrToName :: TyVarBndr flag -> Name tyVarBndrToName = \case PlainTV name _ -> name KindedTV name _ _ -> name