f-o-a-m / kepler

A Haskell framework that facilitates writing ABCI applications
https://kepler.dev
Apache License 2.0
34 stars 10 forks source link

module api sketch #64

Closed safareli closed 4 years ago

safareli commented 5 years ago
type Transfer {from :: Address, to :: Address, amount :: Token}

data BankKeeper m =
  BankKeeper
    { getBalance :: Address -> m Token
    , transfer :: MonadThrow TransferError m => Transfer -> m Unit
    , supply :: m Token
    }

type BankQueryRoute
    = "supply" :-> Token
  <|> "getBalance" :/ Address :-> Token

type BankMsgRoute =
  "transfer" :? Transfer -> Unit

mkBankModule
  :: HasCodec Bank cdc
  => IO
        ( Module BankQueryRoute BankMsgRoute (ReaderT (Store cdc) IO)
        , BankKeeper IO
        )

----------------------------------------------------------------

mkUserModule
  :: HasCodec User cdc
  => BankKeeper IO
  -> IO
        ( Module UserQueryRoute UserMsgRoute (ReaderT (Store cdc) IO)
        , UserKeeper IO
        )

mkPetModule
  :: HasCodec Pet cdc
  => BankKeeper IO
  -> IO
        ( Module PetQueryRoute PetMsgRoute (ReaderT (Store cdc) IO)
        , PetKeeper IO
        )

mkMainModule
  :: HasCodec Main cdc
  => BankKeeper IO
  -> UserKeeper IO
  -> PetKeeper IO
  -> IO
  ( Module MainQueryRoute MainMsgRoute (ReaderT (Store cdc) IO)
  , MainKeeper IO
  )
do 
  (bankM, bankK) <- mkBankModule
  (userM, userK) <- mkUserModule bankK
  (petM, petK) <- mkPetModule bankK
  (mainM, _) mkMainModule bankK userK petK
  store <- mkStore
  toApp $ hoistModule' (runReaderT store) $ mkRootModule
    { bank: bankM
    , user: userM
    , pet: petM
    , main: mainM
    }

-- some type level stuff which works like this
mkRootModule
  ::{ bank :: Module BankQueryRoute BankMsgRoute (ReaderT (Store [Bank]) IO)
    , user :: Module UserQueryRoute UserMsgRoute (ReaderT (Store [User]) IO)
    }
  -> RootModule
      (bank :/ BankQueryRoute <|> user :/ UserQueryRoute)
      (bank :/ BankMsgRoute <|> user :/ UserMsgRoute)
      (ReaderT (Store [Bank, User]) IO)

type Module queryRoute msgRoute m = Module' Identity queryRoute msgRoute m
type RootModule queryRoute msgRoute m = Module' (Map String) queryRoute msgRoute m

Module'
  (queryRoute :: Path_Param_Route)
  (msgRoute :: Path_Param_Route)
  (m :: Type -> Type)
  =
    Module'
      { genesis :: f JSON -> m ()
      , beginBlock :: RequestBeginBlock -> m ResponseBeginBlock
      , endBlock :: RequestEndBlock -> m ResponseEndBlock
      , execute :: RouteHandler msgRoute m
      , query :: RouteHandler queryRoute m
      }

-- | given natural transformation from m to g changes base monad of Module from m  to g
hoistModule'
  :: m ~> g
  -> Module' f queryRoute msgRoute m
  -> Module' f queryRoute msgRoute g

toApp :: RootModule queryRoute msgRoute IO -> App IO
safareli commented 5 years ago

actually here:

      , beginBlock :: RequestBeginBlock -> m ResponseBeginBlock
      , endBlock :: RequestEndBlock -> m ResponseEndBlock

results here should instead be some monoid so responses from all modules can be combined.

martyall commented 5 years ago

This is starting to look pretty good. Couple of things to think about going forward:

  1. Are you implying the Keeper someone outside the module, because i see things here like
mkUserModule
  :: HasCodec User cdc
  => BankKeeper IO
  -> IO
        ( Module UserQueryRoute UserMsgRoute (ReaderT (Store cdc) IO)
        , UserKeeper IO
        )

where the tuple return type to me means "yes". What about the comparison to Halogen where the Keeper is effectively the Halogen.Query a type, and arguable also the Halogen.Message a type that the component can raise even though I'm not sure what raise would mean here. It seems like the functionality listed here is still covered.

  1. Just so everyone's on the same page, my understanding is that this url style querying is meant for external clients only, and that all inter-module communication is done by simple message passing. This means that it's possible we can omit the queryRoute parameter from the type of Module and instead do something like:

type family QueryApi :: Type -> Type

type instance QueryApi BankModule = BankApi

type instance QueryApi UserModule = UserApi

type instance QueryApi RootModule = BankApi :<|> UserApi

server :: RouteT (QueryApi Rootmodule) m
server = bankHandlers :<|> userHandlers

it would make the types much shorter and make it harder for module A to use the api of module B even if it had a reference to the type or module

martyall commented 5 years ago

Also just to clear up the example code you've posted, I don't see the need for strings in the *Msg routes, would they be used for something or can we remove them for clarity

martyall commented 5 years ago

Also with regard the the Begin/EndBlock monoid instances, probably the return type for these hooks is not actually m Response.BeginBlock or m Response.EndBlock but more like m () at the module level, in which case there is already monoid instance for a -> m (). If the RootModule is guaranteed that those hooks are run in order, then it can assemble the Response.EndBlock response to the tendermint-core server using whatever implicit state changes that those hooks made.

There's not a lot of examples of this in their docs, most have to do with updating the modules that govern consensus and voting rights, i.e. token balances

safareli commented 5 years ago
  1. Are you implying the Keeper someone outside the module, because i see things here like

Yes.

What about the ... Message functionality if needed can be achived by having having something like this subscribe :: BusW UpdateMsgs -> IO () in Keeper

  1. Just so everyone's on the same page, my understanding is that this url style querying is meant for external clients only, and that all inter-module communication is done by simple message passing.

Yes url style execute & query handlers are for external use. if some functionality is needed from other module Keepers are for that. Keeper is basically "internal-public" api. Also technically we we can hide constructor of the Module.

This means that it's possible we can omit the queryRoute parameter from the type of Module and instead do something like: ...

for writing handlers you would need access to keepers. also handlers will need same things a module would need.

we might do something like this:


mkUserModule
  :: HasCodec User cdc
  => BankKeeper IO
  -> IO
        ( Module (ReaderT (Store cdc) IO)
        , UserKeeper IO
        , RouteHandler UserQueryRoute (ReaderT (Store cdc) IO)
        , RouteHandler UserMsgRoute (ReaderT (Store cdc) IO)
        )

and assemble handlers by hand.

Also just to clear up the example code you've posted, I don't see the need for strings in the *Msg routes, would they be used for something or can we remove them for clarity

the strings from *Msg, would be used to construct message / query, so given

type BankQueryRoute
    = "supply" :-> Token
  <|> "getBalance" :/ Address :-> Token

type BankMsgRoute =
  "transfer" :? Transfer -> Unit

...
app = do
  ....
  toApp $ hoistModule' (runReaderT store) $ mkRootModule
    { bank: bankM
    ...
    }

then for making transfer one would create message using bank/transfer?from=0x002&to=0x003&amount=0x001 or for querying use this: bank/getBalance/0x002

Also with regard the the Begin/EndBlock monoid instances, probably the return type for these hooks is not actually m Response.BeginBlock or m Response.EndBlock but more like m () at the module level, in which case there is already monoid instance for a -> m (). If the RootModule is guaranteed that those hooks are run in order, then it can assemble the Response.EndBlock response to the tendermint-core server using whatever implicit state changes that those hooks made.

I think this handlers should return some monoid which contains logs, events raised (like etherium) maybe stats like "Gas used" etc. and it should be monoid so this result's then would combine and returned to client / published in block (like Events in etherium). or this could be done using MonadLog Stats m or something, as we would need to create such logs when handling Messages too.