Using other monads with Server
So far we have seen only IO monad to be used with Server.
But we can use other monads with it. Although so far
only three types of monads are supported for Servers:
IO-monadReaderT env IOand newtype wrappers on top of itReaderT env (EitherT IO)and new type wrappers on top of it
So the library is limited in monad choice but all of the cases
can cover everything you need from the server.
I personally prefer to just use IO and pass environment around
to handlers. This process can be automated with ReaderT monad.
Let's study how to use ReaderT with the server.
As example we will build a server that contains mutable state.
It has internal counter which we can query and increment.
To store the internal state as shared envirnment for all handlers
we are going to Reader-pattern or server with ReaderT over IO monad.
Reader-pattern
Our server is Json-based but we want custom monad. So we can import the preset module
for Json:
import Mig.Json
also we import ReaderT from mtl library and IORef to store mutable shared state:
import Control.Monad.Reader
import Data.IORef
Let's define a type for our application:
newtype App a = App (ReaderT Env IO a)
deriving newtype (Functor, Applicative, Monad, MonadReader Env, MonadIO, HasServer)
{-| Common shared state
We can put more shared state if we need. Like logger state or some interfaces.
-}
data Env = Env
{ current :: IORef Int
}
-- | Init shared state
initEnv :: IO Env
initEnv = Env <$> newIORef 0
HasServer class
We declare it as newtype-wrapper with ReaderT under the hood.
We can derive all the classes that we need to use it as Reader.
All classes but last are common repertiore of the haskeller.
The last class HasServer is special to mig library.
It can be also auto-derived as the instance for ReaderT+IO is already defined.
The HasServer class defines how to convert our special monad m server
to Server IO. For a reader it defines a method:
renderServer :: Server (ReaderT env m) -> env -> IO (Server IO)
So if we pass the common shared environment env to server
we can use it as Server IO. We need to convert to Server IO
becuase for WAI and warp we can run only Server IO based servers.
As in library mig-wai:
toApplication :: Server IO -> Wai.Application
How to run Reader based server
So to run the readerT server we need to convert it to IO-based server
and we can run it with usual runServer function:
main :: IO ()
main = do
env <- initEnv
putStrLn ("The counter server listens on port: " <> show port)
runServer port . withSwagger def =<< renderServer server env
where
port = 8085
server :: Server App
Here we also add the swagger to the server for easy testing and trying things out with swagger.
Note that we use bind operator:
runServer port =<< renderServer server env
Server with Reader monad
Our server has two routes:
- get - to querry current state
- put - to add some integer to the state
server :: Server App
server =
"counter"
/. [ "get" /. handleGet
, "put" /. handlePut
]
Let's define the get route:
-- | Get handler. It logs the call and returns current state
handleGet :: Get App (Resp Int)
handleGet = Send $ do
logInfo "Call get"
ref <- asks (.current)
liftIO $ ok <$> readIORef ref
-- | Helper to do simple logging
logInfo :: String -> App ()
logInfo message = liftIO $ putStrLn $ "[INFO] " <> message
So we ask for the common mutable state and read it with readIORef function.
Also we use liftIO to lift IO result to App monad.
We just use App monad inside Send-wrapper to create a handler.
LEt's define the put handler:
-- | Put handler. It logs the call and updates
-- the state with integer which is read from URL
handlePut :: Capture "arg" Int -> Get App (Resp ())
handlePut (Capture val) = Send $ do
logInfo $ "Call put with: " <> show val
ref <- asks (.current)
liftIO $ ok <$> atomicModifyIORef' ref (\cur -> (cur + val, ()))
We use atomicModifyIORef' to be safe in presence of concurrent requests.
So we have completed the definition and we can run the app and try it out.
You can find the complete code of the example in the mig repo.
Summary
In this chapter we have learned how to use Reader-monad with mig library.
We can define our custom wrapper for ReaderT+IO and derive instance
of HasServer and we are ready to go.