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-monad
  • ReaderT env IO and newtype wrappers on top of it
  • ReaderT 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.