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 IOandnewtypewrappers 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.
Also we can use any monad which is convertible to IO with function:
hoistServer :: forall a . (m a -> n a) -> Server m -> Server n
The reason why we would like to convert to IO because warp server
convertion function runServer works only for the type Server IO.
So we can use any monad but we would like to convert to IO at the very and
to be able to run our server with warp.
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 environment 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 repertoire of the Haskell.
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
because 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.
Server with Reader monad
Our server has two routes:
get- to query current stateput- 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 -> Post 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.
Using custom monad
We have studied how to use ReaderT IO and newtype-wrappers on top of it
as monads for our server. To use any other monad we need to have the function:
runAsIO :: MyMonad a -> IO a
For custom monad MyMonad. If there is such a function we can use function:
hoistServer :: forall a . (m a -> n a) -> Server m -> Server n
Prior to call to runServer and run the server which is based on our custom monad:
main :: IO ()
main = runServer 8085 (hoistServer runAsIO server)
server :: Server MyMonad
server = ...
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.