packages feed

method-0.4.0.0: src/Test/Method/Label.hs

{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}

-- |
-- Module : Test.Method.Label
-- Description:
-- License: BSD-3
-- Maintainer: autotaker@gmail.com
-- Stability: experimental
module Test.Method.Label (Label (..), (:|:) (..), deriveLabel) where

import Control.Method (Method (Args, Base))
import Control.Monad.IO.Class (MonadIO)
import Data.Char (isLower, toUpper)
import qualified Data.Kind as K
import qualified Data.List as L
import qualified Data.Map.Strict as M
import Data.Typeable (Typeable)
import Language.Haskell.TH
  ( Dec,
    DecQ,
    DecsQ,
    Name,
    Pred,
    Q,
    TyVarBndr (KindedTV, PlainTV),
    Type (AppT, ArrowT, ConT, ForallT, InfixT, ListT, SigT, TupleT, VarT),
    appE,
    appT,
    caseE,
    conE,
    conP,
    conT,
    cxt,
    dataD,
    gadtC,
    instanceD,
    lam1E,
    match,
    mkName,
    nameBase,
    newName,
    normalB,
    pprint,
    stringE,
    tySynEqn,
    tySynInstD,
    valD,
    varE,
    varP,
    varT,
  )
import qualified Language.Haskell.TH.Datatype as D
import Test.Method.Dynamic (Dynamic, DynamicShow, castMethod)

-- | Type class that represents @f@ denotes the type of field names of @InterfaceOf f@
class Typeable f => Label (f :: K.Type -> K.Type) where
  -- | Interface type corrensponding to @f@
  type InterfaceOf f

  -- | Construct a interface from polymorphic function that returns each field of the interface.
  toInterface ::
    ( forall m.
      ( Typeable m,
        Method m,
        MonadIO (Base m),
        Show (Args m)
      ) =>
      f m ->
      m
    ) ->
    InterfaceOf f

  showLabel :: f m -> String

  compareLabel :: f m1 -> f m2 -> Ordering
  compareLabel x y = showLabel x `compare` showLabel y

-- | @f :|: g@ is the disjoint union of label @f@ and label @g@.
-- Use this type when you want to specify a protocol for multiple interfaces.
--
-- ==== Example
--
-- @
-- data FooService = FooService {
--   foo :: Int -> IO Bool,
--   ...
--   }
-- data BarService = BarService {
--   bar :: String -> IO (),
--   ...
--   }
-- deriveLabel ''FooService
-- deriveLabel ''BarService
--
-- proto :: ProtocolM (FooServiceLabel ':|:' BarServiceLabel) ()
-- proto = do
--   i1 <- decl $ whenArgs ('L' Foo) (==1) \`thenReturn\` True
--   void $ decl $ whenArgs ('R' Bar) (=="bar") \`thenReturn\` () \`dependsOn\` [i1]
--
-- main :: IO ()
-- main = withProtocol proto $ \\(fooService, barService) -> do
--   ...
-- @
data (:|:) f g a = L (f a) | R (g a)
  deriving (Eq, Ord, Show)

instance (Label f, Label g) => Label (f :|: g) where
  type InterfaceOf (f :|: g) = (InterfaceOf f, InterfaceOf g)
  toInterface k = (f, g)
    where
      f = toInterface (k . L)
      g = toInterface (k . R)
  showLabel (L x) = "L " <> showLabel x
  showLabel (R x) = "R " <> showLabel x

-- |
-- Generate the label type from given interface type.
--
-- * Define GADT @XXXLabel m@ for interface @XXX@.
--
--     * @FieldX :: XXXLabel X@ for each field @fieldX :: X@ where @X@ is a standard type.
--     * @PolyFieldX :: XXXLabel ty[Dynamic/a]@ for each field of the form @polyFieldX :: (forall a. Typeable a => ty)@
--
--         * Type variable @a@ is substituted with @DynamicShow@ if @a@ is instances of 'Show' and 'Typeable'
--         * Type variable @a@ is substituted with @Dynamic@ if @a@ is an instance of 'Typeable' but not 'Show'
--         * Report an error if type variable @a@ is not an instance of 'Typeable'
--
-- * Define instance @Label XXXLabel@.
--
-- ==== Example
--
-- @
-- data API env = API {
--     _foo :: Int -> RIO env Int,
--     _bar :: forall a. (Show a, Typeable a) => String -> RIO env (Maybe a),
--     _baz :: forall b. (Typeable a) => b -> RIO env ()
--   }
-- @
--
-- @deriveLabel ''API@ will generate the following code.
--
-- @
-- data APILabel env m where
--     Foo :: APILabel env (Int -> RIO env Int)
--     Bar :: APILabel env (String -> RIO env (Maybe DynamicShow)) -- type variable \`a\` is replaced with 'DynamicShow'
--     Baz :: APILabel env (Dynamic -> RIO env ()) -- type variable \'b\' is replaced with 'Dynamic'
--
-- instance Label (APILabel env) where
--     type InterfaceOf (APILabel env) = API env
--     toInterface k = API (k Foo) (castMethod (k Bar)) (castMethod (k Baz))
--     showLabel x = case x of
--       Foo -> "Foo"
--       Bar -> "Bar"
--       Baz -> "Baz"
-- @
deriveLabel :: Name -> DecsQ
deriveLabel name = do
  info <- D.reifyDatatype name
  tyVars <- mapM extractTyVar $ D.datatypeInstTypes info
  consInfo <- case D.datatypeCons info of
    [consInfo] -> pure consInfo
    _ -> fail $ "Multiple constructors: " <> pprint name
  fieldNames <- case D.constructorVariant consInfo of
    D.RecordConstructor names -> pure names
    _ -> fail $ "Constructor must be a record: " <> pprint (D.constructorName consInfo)
  labelConNames <- mapM fieldToLabel fieldNames
  let fields = D.constructorFields consInfo
      labelTy = foldl ((. VarT) . AppT) (ConT labelName) tyVarNames
      labelName = mkName $ (++ "Label") $ nameBase name
      tyVarNames = map bndrToName tyVars
      recordTy = D.datatypeType info
      recordConName = D.constructorName consInfo
  labelDec <- deriveLabelData (tyVars, fields, labelTy, labelName, labelConNames)
  labelInstDec <- deriveLabelInst (tyVars, labelTy, recordTy, recordConName, labelConNames)
  pure [labelDec, labelInstDec]

deriveLabelInst :: ([TyVarBndr], Type, Type, Name, [Name]) -> DecQ
deriveLabelInst (tyVars, labelTy, interfaceTy, interfaceConName, labelConNames) =
  instanceD (cxt cxts) (conT ''Label `appT` pure labelTy) [interfaceOfDec, toInterfaceDec, showLabelDec]
  where
    cxts = [conT ''Typeable `appT` varT n | n <- bndrToName <$> tyVars]
    interfaceOfDec = tySynInstD (tySynEqn Nothing (conT ''InterfaceOf `appT` pure labelTy) (pure interfaceTy))
    -- toInterface = \k -> API (castMethod $ k Foo) (castMethod $ k Bar) (castMethod $ k Baz)
    toInterfaceDec = valD (varP 'toInterface) (normalB bodyE) []
      where
        k = mkName "k"
        bodyE = lam1E (varP k) $ foldl step (conE interfaceConName) labelConNames
        step acc labelCon = acc `appE` appE (varE 'castMethod) (appE (varE k) (conE labelCon))
    showLabelDec = valD (varP 'showLabel) (normalB bodyE) []
      where
        x = mkName "x"
        bodyE = lam1E (varP x) (caseE (varE x) (map showCase labelConNames))
        showCase conName =
          match (conP conName []) (normalB $ stringE $ nameBase conName) []

-- |
-- @
-- data API env = API {
--   _foo :: RIO env Int,
--   _bar :: Text -> RIO env Bool,
-- }
-- >>>
-- data APILabel env m where
--   Foo :: APILabel env (RIO env Int)
--   Bar :: APILabel env (Text -> RIO env Bool)
-- @
deriveLabelData :: ([TyVarBndr], [Type], Type, Name, [Name]) -> Q Dec
deriveLabelData (tyVars, fields, labelTy, labelName, labelConNames) = do
  m <- newName "m"
  dataD (cxt []) labelName (tyVars ++ [PlainTV m]) Nothing consQ []
  where
    consQ = zipWith toLabel fields labelConNames
    --  $cName :: $labelName x1 ... xn $fieldTy
    toLabel fieldTy cName = gadtC [cName] [] (pure labelTy `appT` unquantify fieldTy)

unquantify :: Type -> Q Type
unquantify _ty@(ForallT bndrs ctx ty) = do
  tbl <- M.fromList <$> mapM (substBndr _ty ctx) bndrs
  subst tbl ty
unquantify ty = pure ty

substBndr :: Type -> [Pred] -> TyVarBndr -> Q (Name, Type)
substBndr ty preds = go . bndrToName
  where
    go n
      | (ConT ''Typeable `AppT` VarT n) `elem` preds
          && (ConT ''Show `AppT` VarT n) `elem` preds =
        pure (n, ConT ''DynamicShow)
      | (ConT ''Typeable `AppT` VarT n) `elem` preds = pure (n, ConT ''Dynamic)
      | otherwise =
        fail $
          "cannot unquantify: " <> pprint ty
            <> " because Typeable "
            <> pprint n
            <> " constraint is missing"

subst :: M.Map Name Type -> Type -> Q Type
subst tbl (VarT x) = case M.lookup x tbl of
  Just t -> pure t
  Nothing -> pure $ VarT x
subst tbl (AppT f x) = AppT <$> subst tbl f <*> subst tbl x
subst tbl (InfixT x op y) = InfixT <$> subst tbl x <*> pure op <*> subst tbl y
subst _ ArrowT = pure ArrowT
subst _ ty@ConT {} = pure ty
subst _ ty@TupleT {} = pure ty
subst _ ty@ListT = pure ty
subst _ ty@ForallT {} = fail $ "nested forall quantifier is not supported: " <> pprint ty
subst _ ty = fail $ "conversion for " <> pprint ty <> " is not implemented yet. Please raise an issue."

bndrToName :: TyVarBndr -> Name
bndrToName (PlainTV n) = n
bndrToName (KindedTV n _) = n

-- |
-- @
-- fieldToLabel (mkName "_hello") = pure (mkName \"Hello\")
-- @
fieldToLabel :: Name -> Q Name
fieldToLabel fieldName = toConName trimed
  where
    base = nameBase fieldName
    trimed = L.dropWhile (not . isLower) base
    toConName "" = fail $ "cannot convert field name to constructor name: " <> pprint fieldName
    toConName (x : xs) = pure $ mkName $ toUpper x : xs

extractTyVar :: Type -> Q TyVarBndr
extractTyVar (SigT (VarT x) k) = pure $ KindedTV x k
extractTyVar (VarT x) = pure $ PlainTV x
extractTyVar ty = fail $ "cannot extract type variable: " <> pprint ty