Instance template (#33)

* protocol instance template [WIP]

* protocol instances template

* add methods to check correctness of participant types in protocol TH

* PushConfirm and and PushMsg implementation types

* check Command type + doctest
This commit is contained in:
Evgeny Poberezkin
2020-05-14 21:30:37 +01:00
committed by GitHub
parent aa2ac80cf9
commit bdec751725
8 changed files with 262 additions and 118 deletions
+13
View File
@@ -19,7 +19,9 @@ ghc-options:
- -Wincomplete-uni-patterns - -Wincomplete-uni-patterns
default-extensions: default-extensions:
- BlockArguments
- DuplicateRecordFields - DuplicateRecordFields
- LambdaCase
- NamedFieldPuns - NamedFieldPuns
- NoImplicitPrelude - NoImplicitPrelude
- OverloadedStrings - OverloadedStrings
@@ -35,10 +37,12 @@ dependencies:
- servant-docs - servant-docs
- servant-server - servant-server
- template-haskell - template-haskell
# - pretty-show
library: library:
source-dirs: src source-dirs: src
exposed-modules: exposed-modules:
- Predicate
- Simplex.Messaging.Types - Simplex.Messaging.Types
- Simplex.Messaging.ServerAPI - Simplex.Messaging.ServerAPI
@@ -46,3 +50,12 @@ executables:
api-docs: api-docs:
source-dirs: src source-dirs: src
main: Main.hs main: Main.hs
tests:
simplex-definitions-doctests:
source-dirs: tests
main: doctest-driver.hs
ghc-options: -threaded
dependencies:
- doctest
- doctest-driver-gen
+22 -10
View File
@@ -5,8 +5,7 @@ module Predicate where
import ClassyPrelude import ClassyPrelude
import Data.Type.Predicate import Data.Type.Predicate
import Data.Type.Predicate.Auto import Data.Type.Predicate.Auto
import Language.Haskell.TH.Lib import Language.Haskell.TH
import Language.Haskell.TH.Syntax
-- This template adds instances of Auto typeclass (from decidable package) -- This template adds instances of Auto typeclass (from decidable package)
-- to a given parametrised type definition -- to a given parametrised type definition
@@ -19,7 +18,7 @@ import Language.Haskell.TH.Syntax
-- data T (a :: P) where -- data T (a :: P) where
-- TA :: T 'A -- TA :: T 'A
-- TB :: T 'B -- TB :: T 'B
-- |] -- |])
-- --
-- `predicate` splice will add these instances: -- `predicate` splice will add these instances:
-- --
@@ -32,15 +31,28 @@ predicate :: Q [Dec] -> Q [Dec]
predicate decls = concat <$> (decls >>= mapM addInstances) predicate decls = concat <$> (decls >>= mapM addInstances)
where where
addInstances :: Dec -> Q [Dec] addInstances :: Dec -> Q [Dec]
addInstances d@(DataD _ ty _ _ constructors _) = do addInstances d@(DataD _ _ _ _ constructors _) = do
ds <- mapM (mkInstance ty) constructors ds <- mapM mkInstance constructors
return $ d : concat ds if any null ds
addInstances d = return [d] then do
reportWarning $
"warning: not a parametrised GADT predicate (no instances added)\n"
++ pprint d
return [d]
else
return $ d : concat ds
addInstances d = do
reportWarning $ "warning: not a data type declaration\n" ++ pprint d
return [d]
mkInstance :: Name -> Con -> Q [Dec] mkInstance :: Con -> Q [Dec]
mkInstance ty (GadtC [con] [] (AppT _ (PromotedT p))) = mkInstance (GadtC
[con] _
(AppT
(ConT ty)
(PromotedT p))) =
[d| [d|
instance Auto (TyPred $(conT ty)) $(promotedT p) where instance Auto (TyPred $(conT ty)) $(promotedT p) where
auto = $(conE con) auto = $(conE con)
|] |]
mkInstance _ _ = return [] mkInstance _ = return []
+28 -33
View File
@@ -1,52 +1,47 @@
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
-- {-# OPTIONS_GHC -ddump-splices #-}
{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
module Simplex.Messaging.Broker where module Simplex.Messaging.Broker where
import ClassyPrelude import ClassyPrelude
import Data.Singletons.TH
import Simplex.Messaging.Protocol import Simplex.Messaging.Protocol
import Simplex.Messaging.Types import Simplex.Messaging.Types
$(protocol Broker [d|
instance Prf HasState Sender s bcCreateConn = CreateConn <-- Recipient
=> ProtocolCommand Broker bcSubscribe = Subscribe <-- Recipient
Recipient baPushConfirm = PushConfirm --> Recipient
CreateConnRequest CreateConnResponse baPushMsg = PushMsg --> Recipient
(None <==> None <==| s) |])
(New <==> New <==| s)
Idle Idle 0 0
where
command = CreateConn
protoCmd = bCreateConn
instance ( (r /= None && r /= Disabled) ~ True
, (b /= None && b /= Disabled) ~ True
, Prf HasState Sender s )
=> ProtocolCommand Broker
Recipient
() ()
(r <==> b <==| s)
(r <==> b <==| s)
Idle Subscribed n n
where
command = Subscribe
protoCmd = bSubscribe
bCreateConn :: Connection Broker None Idle bcCreateConn :: Connection Broker None Idle
-> CreateConnRequest -> CreateConnRequest
-> Either String (CreateConnResponse, Connection Broker New Idle) -> Either String (CreateConnResponse, Connection Broker New Idle)
bCreateConn = protoCmdStub bcCreateConn = protoCmdStub
bSubscribe :: Connection Broker s Idle bcSubscribe :: Connection Broker s Idle
-> () -> ()
-> Either String ((), Connection Broker s Subscribed) -> Either String ((), Connection Broker s Subscribed)
bSubscribe = protoCmdStub bcSubscribe = protoCmdStub
baPushConfirm :: Connection Broker New Subscribed
-> SecureConnRequest
-> Either String ()
-> Either String (Connection Broker New Subscribed)
baPushConfirm = protoActionStub
baPushMsg :: Connection Broker Secured Subscribed
-> MessagesResponse -- TODO, has to be a single message
-> Either String ()
-> Either String (Connection Broker Secured Subscribed)
baPushMsg = protoActionStub
+28 -42
View File
@@ -1,62 +1,48 @@
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-} {-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
-- {-# OPTIONS_GHC -ddump-splices #-}
{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
module Simplex.Messaging.Client where module Simplex.Messaging.Client where
import ClassyPrelude import ClassyPrelude
import Data.Singletons.TH
import Simplex.Messaging.Protocol import Simplex.Messaging.Protocol
import Simplex.Messaging.Types import Simplex.Messaging.Types
-- $(protocol Recipient [d| $(protocol Recipient [d|
-- raCreateConn :: (--> Broker) CreateConn raCreateConn = CreateConn --> Broker
-- raSubscribe :: (--> Broker) Subscribe raSubscribe = Subscribe --> Broker
-- rcPushConfirm :: (<-- Broker) PushConfirm rcPushConfirm = PushConfirm <-- Broker
-- rcPushMsg :: (<-- Broker) PushMsg rcPushMsg = PushMsg <-- Broker
-- ... |])
-- |]
instance Prf HasState Sender s
=> ProtocolAction Recipient
Broker
CreateConnRequest CreateConnResponse
(None <==> None <==| s)
(New <==> New <==| s)
Idle Idle 0 0
where
action = CreateConn
protoAction = rCreateConn
instance ( (r /= None && r /= Disabled) ~ True
, (b /= None && b /= Disabled) ~ True
, Prf HasState Sender s )
=> ProtocolAction Recipient
Broker
() ()
(r <==> b <==| s)
(r <==> b <==| s)
Idle Subscribed n n
where
action = Subscribe
protoAction = rSubscribe
rCreateConn :: Connection Recipient None Idle raCreateConn :: Connection Recipient None Idle
-> CreateConnRequest -> CreateConnRequest
-> Either String CreateConnResponse -> Either String CreateConnResponse
-> Either String (Connection Recipient New Idle) -> Either String (Connection Recipient New Idle)
rCreateConn = protoActionStub raCreateConn = protoActionStub
rSubscribe :: Connection Recipient s Idle raSubscribe :: Connection Recipient s Idle
-> () -> ()
-> Either String () -> Either String ()
-> Either String (Connection Recipient s Subscribed) -> Either String (Connection Recipient s Subscribed)
rSubscribe = protoActionStub raSubscribe = protoActionStub
rcPushConfirm :: Connection Recipient Pending Subscribed
-> SecureConnRequest
-> Either String ((), Connection Recipient Confirmed Subscribed)
rcPushConfirm = protoCmdStub
rcPushMsg :: Connection Recipient Secured Subscribed
-> MessagesResponse -- TODO, has to be a single message
-> Either String ((), Connection Recipient Secured Subscribed)
rcPushMsg = protoCmdStub
+146 -33
View File
@@ -21,6 +21,7 @@
module Simplex.Messaging.Protocol where module Simplex.Messaging.Protocol where
import ClassyPrelude import ClassyPrelude
import Control.Monad.Fail
import Data.Kind import Data.Kind
import Data.Singletons import Data.Singletons
import Data.Singletons.ShowSing import Data.Singletons.ShowSing
@@ -28,11 +29,15 @@ import Data.Singletons.TH
import Data.Type.Predicate import Data.Type.Predicate
import Data.Type.Predicate.Auto import Data.Type.Predicate.Auto
import GHC.TypeLits import GHC.TypeLits
import Language.Haskell.TH hiding (Type)
import qualified Language.Haskell.TH as TH
import Predicate import Predicate
import Simplex.Messaging.Types import Simplex.Messaging.Types
-- import Text.Show.Pretty (ppShow)
$(singletons [d| $(singletons [d|
data Participant = Recipient | Broker | Sender data Participant = Recipient | Broker | Sender
deriving (Show, Eq)
data ConnectionState = None -- (all) not available or removed from the broker data ConnectionState = None -- (all) not available or removed from the broker
| New -- (participants: all) connection created (or received from sender) | New -- (participants: all) connection created (or received from sender)
@@ -120,14 +125,21 @@ deriving instance Show (rs <==> bs <==| ss)
st2 :: Pending <==> New <==| Confirmed st2 :: Pending <==> New <==| Confirmed
st2 = SPending :<==> SNew :<==| SConfirmed st2 = SPending :<==> SNew :<==| SConfirmed
-- this must not type check -- | Using not allowed connection type results in type error
--
-- >>> :{
-- stBad :: 'Pending <==> 'Confirmed <==| 'Confirmed -- stBad :: 'Pending <==> 'Confirmed <==| 'Confirmed
-- stBad = SPending :<==> SConfirmed :<==| SConfirmed -- stBad = SPending :<==> SConfirmed :<==| SConfirmed
-- :}
-- ...
-- ... No instance for (Auto (TyPred BrokerCS) 'Confirmed)
-- ... arising from a use of ‘:<==>’
-- ...
infixl 4 :>>, :>>= infixl 4 :>>, :>>=
data Command arg result data Command arg res
(from :: Participant) (to :: Participant) (from :: Participant) (to :: Participant)
state state' state state'
(ss :: ConnSubscription) (ss' :: ConnSubscription) (ss :: ConnSubscription) (ss' :: ConnSubscription)
@@ -239,20 +251,6 @@ data Command arg result
Fail :: String Fail :: String
-> Command a String from to state (None <==> None <==| None) ss ss n n -> Command a String from to state (None <==> None <==| None) ss ss n n
-- redifine Monad operators to compose commands
-- using `do` notation with RebindableSyntax extension
(>>=) :: Command a b from1 to1 s1 s2 ss1 ss2 n1 n2
-> (b -> Command b c from2 to2 s2 s3 ss2 ss3 n2 n3)
-> Command a c from1 to2 s1 s3 ss1 ss3 n1 n3
(>>=) = (:>>=)
(>>) :: Command a b from1 to1 s1 s2 ss1 ss2 n1 n2
-> Command c d from2 to2 s2 s3 ss2 ss3 n2 n3
-> Command a d from1 to2 s1 s3 ss1 ss3 n1 n3
(>>) = (:>>)
fail :: String -> Command a String from to state (None <==> None <==| None) ss ss n n
fail = Fail
-- show and validate expexcted command participants -- show and validate expexcted command participants
infix 6 --> infix 6 -->
@@ -270,42 +268,157 @@ type family PConnSt (p :: Participant) state where
-- Type classes to ensure consistency of types of implementations -- Type classes to ensure consistency of types of implementations
-- of participants actions/functions and connection state transitions (types) -- of participants commands/actions and connection state transitions (types)
-- with types of protocol commands defined above. -- with types of protocol commands defined above.
-- One participant's command is another participant's action.
class ProtocolCommand (p :: Participant) class ProtocolCommand
(from :: Participant)
arg res arg res
(from :: Participant) (to :: Participant)
state state' state state'
(ss :: ConnSubscription) (ss' :: ConnSubscription) (ss :: ConnSubscription) (ss' :: ConnSubscription)
(n :: Nat) (n' :: Nat) (messages :: Nat) (messages' :: Nat)
where where
command :: Command arg res from p state state' ss ss' n n' command :: Command arg res from to state state' ss ss' messages messages'
protoCmd :: Connection p (PConnSt p state) ss cmdMe :: Sing to
cmdOther :: Sing from
protoCmd :: Connection to (PConnSt to state) ss
-> arg -> arg
-> Either String (res, Connection p (PConnSt p state') ss') -> Either String (res, Connection to (PConnSt to state') ss')
protoCmdStub :: Connection p ps ss protoCmdStub :: Connection to ps ss
-> arg -> arg
-> Either String (res, Connection p ps' ss') -> Either String (res, Connection to ps' ss')
protoCmdStub _ _ = Left "Command not implemented" protoCmdStub _ _ = Left "Command not implemented"
class ProtocolAction (p :: Participant) class ProtocolAction
(to :: Participant)
arg res arg res
(from :: Participant) (to :: Participant)
state state' state state'
(ss :: ConnSubscription) (ss' :: ConnSubscription) (ss :: ConnSubscription) (ss' :: ConnSubscription)
(n :: Nat) (n' :: Nat) (messages :: Nat) (messages' :: Nat)
where where
action :: Command arg res p to state state' ss ss' n n' action :: Command arg res from to state state' ss ss' messages messages'
protoAction :: Connection p (PConnSt p state) ss actionMe :: Sing from
actionOther :: Sing to
protoAction :: Connection from (PConnSt from state) ss
-> arg -> arg
-> Either String res -> Either String res
-> Either String (Connection p (PConnSt p state') ss') -> Either String (Connection from (PConnSt from state') ss')
protoActionStub :: Connection p ps ss protoActionStub :: Connection from ps ss
-> arg -> arg
-> Either String res -> Either String res
-> Either String (Connection p ps' ss') -> Either String (Connection from ps' ss')
protoActionStub _ _ _ = Left "Action not implemented" protoActionStub _ _ _ = Left "Action not implemented"
-- TH to generate typeclasse instances for ProtocolCommand
-- and ProtocolAction classes from implementation definition syntax.
--
-- Given this declaration:
--
-- $(protocol Recipient [d|
-- rcPushConfirm = PushConfirm <-- Broker
-- raCreateConn = CreateConn --> Broker
-- |])
--
-- two instance declarations will be generated:
-- - for ProtocolCommand with `protoCmd = rcPushConfirm` and `command = PushConfirm`
-- - for ProtocolAction class with `protoAction = raCreateConn` and `action = CreateConn`
-- matching PushConfirm and CreateConn constructors types
-- Type definitions for functions rcPushConfirm and raCreateConn have to be written manually -
-- they have to be consistent with the typeclass.
data ProtocolOpeartion = POCommand | POAction | POUndefined
data ProtocolClassDescription = ProtocolClassDescription
{ clsName :: String
, mthd :: String
, meSing :: String
, otherSing :: String
, protoMthd :: String }
protocol :: Participant -> Q [Dec] -> Q [Dec]
protocol me ds = ds >>= mapM mkProtocolInstance
where
getCls :: ProtocolOpeartion -> Maybe ProtocolClassDescription
getCls POUndefined = Nothing
getCls POCommand = Just ProtocolClassDescription
{ clsName = "ProtocolCommand"
, mthd = "command"
, meSing = "cmdMe"
, otherSing = "cmdOther"
, protoMthd = "protoCmd" }
getCls POAction = Just ProtocolClassDescription
{ clsName = "ProtocolAction"
, mthd = "action"
, meSing = "actionMe"
, otherSing = "actionOther"
, protoMthd = "protoAction" }
mkProtocolInstance :: Dec -> Q Dec
mkProtocolInstance d = case d of
ValD
(VarP fName)
(NormalB
(InfixE
(Just (ConE cmd))
opExp
(Just (ConE other)))) [] ->
case getCls (getOperation opExp) of
Nothing -> badSyntax d
Just cls -> instanceDecl d fName cmd other cls
_ -> badSyntax d
instanceDecl :: Dec
-> Name -> Name -> Name
-> ProtocolClassDescription
-> Q Dec
instanceDecl d fName cmd other ProtocolClassDescription{..} =
reify cmd >>= \case
DataConI _ (ForallT _ ctxs ty) _ -> do
tc <- changeTyCon d ty clsName
return $
InstanceD Nothing ctxs tc
[ mkMethod mthd (ConE cmd)
, mkMethod meSing (mkSing $ show me)
, mkMethod otherSing (mkSing $ nameBase other)
, mkMethod protoMthd (VarE . mkName $ nameBase fName) ]
_ -> badSyntax d
mkMethod :: String -> Exp -> Dec
mkMethod name rhs = ValD (VarP (mkName name)) (NormalB rhs) []
mkSing :: String -> Exp
mkSing = ConE . mkName . ('S':)
changeTyCon :: Dec -> TH.Type -> String -> Q TH.Type
changeTyCon d (AppT t1 t2) n =
(`AppT` t2) <$> changeTyCon d t1 n
changeTyCon d (ConT name) n =
if nameBase name == "Command"
then conT $ mkName n
else badConstructor d
changeTyCon d _ _ = badConstructor d
badConstructor :: Dec -> Q TH.Type
badConstructor d = fail $ "constructor type must be Command\n" ++ pprint d
getOperation :: Exp -> ProtocolOpeartion
getOperation = \case
VarE name -> getOp name
UnboundVarE name -> getOp name
_ -> POUndefined
where
getOp n = case nameBase n of
"<--" -> POCommand
"-->" -> POAction
_ -> POUndefined
badSyntax :: Dec -> Q Dec
badSyntax d = fail $ "invalid protocol syntax: " ++ pprint d
-- introspect :: Name -> Q Exp
-- introspect n = reify n >>= runIO . putStrLn . pack . ppShow >> [|return ()|]
@@ -0,0 +1,23 @@
{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
module Simplex.Messaging.ProtocolDo where
import ClassyPrelude
import Simplex.Messaging.Protocol
-- redifine Monad operators to compose commands
-- using `do` notation with RebindableSyntax extension
(>>=) :: Command a b from1 to1 s1 s2 ss1 ss2 n1 n2
-> (b -> Command b c from2 to2 s2 s3 ss2 ss3 n2 n3)
-> Command a c from1 to2 s1 s3 ss1 ss3 n1 n3
(>>=) = (:>>=)
(>>) :: Command a b from1 to1 s1 s2 ss1 ss2 n1 n2
-> Command c d from2 to2 s2 s3 ss2 ss3 n2 n3
-> Command a d from1 to2 s1 s3 ss1 ss3 n1 n3
(>>) = (:>>)
fail :: String -> Command a String from to state (None <==> None <==| None) ss ss n n
fail = Fail
@@ -12,6 +12,7 @@ module Simplex.Messaging.Scenarios where
import ClassyPrelude hiding ((>>=), (>>), fail) import ClassyPrelude hiding ((>>=), (>>), fail)
import Data.Singletons import Data.Singletons
import Simplex.Messaging.Protocol import Simplex.Messaging.Protocol
import Simplex.Messaging.ProtocolDo
import Simplex.Messaging.Types import Simplex.Messaging.Types
r :: Sing Recipient r :: Sing Recipient
+1
View File
@@ -0,0 +1 @@
{-# OPTIONS_GHC -F -pgmF doctest-driver-gen -optF src -optF -XBlockArguments -optF -XDuplicateRecordFields -optF -XLambdaCase -optF -XNamedFieldPuns -optF -XNoImplicitPrelude -optF -XOverloadedStrings -optF -XRecordWildCards -optF -XAllowAmbiguousTypes -optF -XConstraintKinds -optF -XDeriveAnyClass -optF -XEmptyCase -optF -XFlexibleContexts -optF -XFlexibleInstances -optF -XGADTs -optF -XInstanceSigs -optF -XMultiParamTypeClasses -optF -XNoStarIsType -optF -XScopedTypeVariables -optF -XStandaloneDeriving -optF -XTemplateHaskell -optF -XTypeApplications -optF -XTypeFamilies -optF -XTypeInType -optF -XTypeOperators -optF -XUndecidableInstances #-}