packages feed

th-lego-0.3: library/THLego/Instances.hs

module THLego.Instances where

import qualified Data.Text as Text
import Language.Haskell.TH
import THLego.Helpers
import qualified THLego.Helpers as Helpers
import qualified THLego.Lambdas as Lambdas
import THLego.Prelude
import qualified TemplateHaskell.Compat.V0208 as Compat

-- * IsLabel

-- |
-- The most general template for 'IsLabel'.
isLabel :: TyLit -> Type -> Exp -> Dec
isLabel label repType fromLabelExp =
  InstanceD Nothing [] headType [fromLabelDec]
  where
    headType =
      multiAppT (ConT ''IsLabel) [LitT label, repType]
    fromLabelDec =
      FunD 'fromLabel [Clause [] body []]
      where
        body =
          NormalB fromLabelExp

-- ** Constructor

-- |
--
-- > instance (a ~ Text) => IsLabel "error" (a -> Result)
constructorIsLabel :: TyLit -> Type -> [Type] -> Exp -> Dec
constructorIsLabel label ownerType memberTypes fromLabelExp =
  InstanceD Nothing paramPreds headType [fromLabelDec]
  where
    paramPreds =
      memberTypes
        & Helpers.mapWithAlphabeticName (\n t -> multiAppT EqualityT [VarT n, t])
    headType =
      multiAppT (ConT ''IsLabel) [LitT label, repType]
      where
        repType =
          arrowChainT memberVarTypes ownerType
          where
            memberVarTypes =
              Helpers.mapWithAlphabeticName (const . VarT) paramPreds
    fromLabelDec =
      FunD 'fromLabel [Clause [] (NormalB fromLabelExp) []]

newtypeConstructorIsLabel :: TyLit -> Type -> Name -> Type -> Dec
newtypeConstructorIsLabel label ownerType conName memberType =
  sumConstructorIsLabel label ownerType conName [memberType]

sumConstructorIsLabel :: TyLit -> Type -> Name -> [Type] -> Dec
sumConstructorIsLabel label ownerType conName memberTypes =
  constructorIsLabel label ownerType memberTypes (ConE conName)

enumConstructorIsLabel :: TyLit -> Type -> Name -> Dec
enumConstructorIsLabel label ownerType conName =
  sumConstructorIsLabel label ownerType conName []

-- |
-- 'IsLabel' instance which converts tuple to ADT.
tupleAdtConstructorIsLabel :: TyLit -> Type -> Name -> [Type] -> Dec
tupleAdtConstructorIsLabel label ownerType conName memberTypes =
  constructorIsLabel label ownerType [memberType] fromLabelExp
  where
    memberType =
      appliedTupleOrSingletonT memberTypes
    fromLabelExp =
      Lambdas.tupleToProduct conName (length memberTypes)

-- ** Mapper

-- |
-- Template of 'IsLabel' for instances mapping to mapper functions.
--
-- > instance (mapper ~ (Text -> Text)) => IsLabel "name" (mapper -> Person -> Person)
mapperIsLabel ::
  -- | Field label.
  TyLit ->
  -- | Type of the product.
  Type ->
  -- | Type of the mapper function.
  Type ->
  -- | 'fromLabel' definition expression.
  Exp ->
  -- | 'IsLabel' instance declaration.
  Dec
mapperIsLabel label ownerType projectionType fromLabelExp =
  InstanceD Nothing [memberPred] headType [fromLabelDec]
  where
    projVarType =
      VarT (mkName "mapper")
    memberPred =
      multiAppT EqualityT [projVarType, projectionType]
    headType =
      multiAppT (ConT ''IsLabel) [LitT label, instanceType]
      where
        instanceType =
          arrowChainT [projVarType, ownerType] ownerType
    fromLabelDec =
      FunD 'fromLabel [Clause [] (NormalB fromLabelExp) []]

-- |
-- Template of 'IsLabel' for instances mapping to mapper functions.
--
-- > instance (mapper ~ (Text -> Text)) => IsLabel "name" (mapper -> Person -> Person)
productMapperIsLabel ::
  -- | Field label.
  TyLit ->
  -- | Type of the product.
  Type ->
  -- | Type of the member we\'re focusing on.
  Type ->
  -- | Constructor name.
  Name ->
  -- | Total amount of members in the product.
  Int ->
  -- | Offset of the member we're focusing on.
  Int ->
  -- | 'IsLabel' instance declaration.
  Dec
productMapperIsLabel label ownerType memberType conName totalMemberTypes offset =
  mapperIsLabel
    label
    ownerType
    (multiAppT ArrowT [memberType, memberType])
    (Lambdas.productMapper conName totalMemberTypes offset)

-- |
-- Template of 'IsLabel' for instances mapping to mapper functions.
--
-- > instance (mapper ~ (Int -> Text -> (Int, Text))) => IsLabel "error" (mapper -> Result -> Result)
sumMapperIsLabel ::
  -- | Field label.
  TyLit ->
  -- | Type of the product.
  Type ->
  -- | Constructor name.
  Name ->
  -- | Member types we\'re focusing on.
  [Type] ->
  -- | 'IsLabel' instance declaration.
  Dec
sumMapperIsLabel label ownerType conName memberTypes =
  mapperIsLabel
    label
    ownerType
    (arrowChainT memberTypes (appliedTupleOrSingletonT memberTypes))
    (Lambdas.sumMapper conName (length memberTypes))

-- ** Accessor

-- |
-- Template of 'IsLabel' for instances mapping to accessor functions.
accessorIsLabel :: TyLit -> Type -> Type -> Exp -> Dec
accessorIsLabel label ownerType projectionType fromLabelExp =
  InstanceD Nothing [memberPred] headType [fromLabelDec]
  where
    projVarType =
      VarT aName
    memberPred =
      multiAppT EqualityT [projVarType, projectionType]
    headType =
      multiAppT (ConT ''IsLabel) [LitT label, instanceType]
      where
        instanceType =
          multiAppT ArrowT [ownerType, projVarType]
    fromLabelDec =
      FunD 'fromLabel [Clause [] (NormalB fromLabelExp) []]

-- |
-- Instance of 'IsLabel' for a member of a product type.
productAccessorIsLabel ::
  -- | Field label.
  TyLit ->
  -- | Type of the product.
  Type ->
  -- | Type of the member we\'re focusing on.
  Type ->
  -- | Constructor name.
  Name ->
  -- | Total amount of members in the product.
  Int ->
  -- | Offset of the member we're focusing on.
  Int ->
  -- | 'IsLabel' instance declaration.
  Dec
productAccessorIsLabel label ownerType projectionType conName numMembers offset =
  accessorIsLabel label ownerType projectionType fromLabelExp
  where
    fromLabelExp =
      Lambdas.productGetter conName numMembers offset

-- |
-- > instance (a ~ Maybe Text) => IsLabel "error" (Result -> a)
sumAccessorIsLabel :: TyLit -> Type -> Name -> [Type] -> Dec
sumAccessorIsLabel label ownerType conName memberTypes =
  accessorIsLabel label ownerType projectionType fromLabelExp
  where
    projectionType =
      AppT (ConT ''Maybe) (appliedTupleOrSingletonT memberTypes)
    fromLabelExp =
      Lambdas.adtConstructorNarrower conName (length memberTypes)

enumAccessorIsLabel :: TyLit -> Type -> Name -> Dec
enumAccessorIsLabel label ownerType conName =
  accessorIsLabel label ownerType projectionType fromLabelExp
  where
    projectionType =
      ConT ''Bool
    fromLabelExp =
      Lambdas.enumConstructorToBool conName

-- * 'HasField'

-- | The most general template for 'HasField'.
hasField :: TyLit -> Type -> Type -> [Clause] -> Dec
hasField fieldLabel ownerType projectionType getFieldFunClauses =
  InstanceD Nothing [] headType [getFieldDec]
  where
    headType =
      multiAppT (ConT ''HasField) [LitT fieldLabel, ownerType, projectionType]
    getFieldDec =
      FunD 'getField getFieldFunClauses

-- |
-- 'HasField' instance which focuses on a variant of an enum
-- and projects it into 'Bool' signaling whether the value matches.
--
-- Generates code of the following pattern:
--
-- > instance HasField "fieldLabel" enumType Bool
enumHasField ::
  -- | Field label.
  TyLit ->
  -- | Enum type.
  Type ->
  -- | Name of the constructor.
  Name ->
  -- | 'HasField' instance declaration.
  Dec
enumHasField fieldLabel ownerType constructorName =
  hasField fieldLabel ownerType projectionType getFieldFunClauses
  where
    projectionType =
      ConT ''Bool
    getFieldFunClauses =
      [matching, unmatching]
      where
        matching =
          Clause [ConP constructorName []] (NormalB bodyExp) []
          where
            bodyExp =
              ConE 'True
        unmatching =
          Clause [WildP] (NormalB bodyExp) []
          where
            bodyExp =
              ConE 'False

-- |
-- Instance of 'HasField' for a constructor of a sum ADT,
-- projecting it into a 'Maybe' tuple of its members.
--
-- Generates code of the following pattern:
--
-- > instance HasField "fieldLabel" sumAdt (Maybe projectionType)
--
-- - When the amount of member types is 0, @projectionType@ is @()@.
-- - When the amount of member types is 1, it is that member type.
-- - Otherwise it is a tuple of those members.
sumHasField ::
  -- | Field label.
  TyLit ->
  -- | The ADT type.
  Type ->
  -- | Name of the constructor.
  Name ->
  -- | Member types of that constructor.
  [Type] ->
  -- | 'HasField' instance declaration.
  Dec
sumHasField fieldLabel ownerType constructorName memberTypes =
  hasField fieldLabel ownerType projectionType getFieldFunClauses
  where
    projectionType =
      AppT (ConT ''Maybe) (appliedTupleOrSingletonT memberTypes)
    getFieldFunClauses =
      [matching, unmatching]
      where
        varNames =
          memberTypes
            & mapWithAlphabeticName (const . id)
        matching =
          Clause [ConP constructorName pats] (NormalB bodyExp) []
          where
            pats =
              fmap VarP varNames
            bodyExp =
              AppE (ConE 'Just) (appliedTupleE (fmap VarE varNames))
        unmatching =
          Clause [WildP] (NormalB bodyExp) []
          where
            bodyExp =
              ConE 'Nothing

-- |
-- Instance of 'HasField' for a member of a product type.
productHasField ::
  -- | Field label.
  TyLit ->
  -- | Type of the product.
  Type ->
  -- | Type of the member we\'re focusing on.
  Type ->
  -- | Constructor name.
  Name ->
  -- | Total amount of members in the product.
  Int ->
  -- | Offset of the member we're focusing on.
  Int ->
  -- | 'HasField' instance declaration.
  Dec
productHasField fieldLabel ownerType projectionType constructorName totalMemberTypes offset =
  hasField fieldLabel ownerType projectionType getFieldFunClauses
  where
    getFieldFunClauses =
      [Clause [ConP constructorName pats] (NormalB bodyExp) []]
      where
        pats =
          replicate offset WildP
            <> bool empty [VarP aName] (totalMemberTypes > 0)
            <> replicate (totalMemberTypes - offset - 1) WildP
        bodyExp =
          VarE aName