Show case for a small application written with servant and Reversed Handle pattern. It is an example on how to make Dependency Injection in Haskell with Handle-pattern.
The main idea of reversed handle pattern is that we build interfaces for external services not driven by services themselves but by the methods we use in concrete API-routes. In the original article on Handle pattern we describe interfaces to DB or Logging based on the natural API of the library. But I argue that it's much more beneficial to create small interfaces dedicated to concrete task of the API-route. This way our interfaces are more flexible and local regarding to change of the code. Also it might be interesting to check the same project implemented in Reader pattern.
Application allows to save messages with tags. User can create a new message and then user can query it by id or by tag.
API methods:
POST: api/v1/save:
input: JSON text of the message and list of tags
output: id of the message
GET: api/v1/get/message/{message-id}
input: id of the message
output: message or error
GET: api/v1/list/tag/{tag}
input: tag
output: list of messages that belong to the tag
POST: api/v1/toggle-logs
toggles the logs (active or silent)
Applications shows how to create interfaces for mocks and real instances.
Also we show how to use interfaces that depend on run-time data
and how we can split the top-level interface to smaller ones dedicated to concrete methods.
On save message is augmented with current time stamp which is queried over external service.
We use getCurrentTime
for mock but it serves as an example of external dependency.
See makefile for available actions for installation and testing the service. The app can be build with stack. The GHC extension list is kept lightweight but we rely on modern compiler GHC 9.2 for nice record-dot syntax.
We can
-
build the app:
make build
-
run the executable:
make run
It will start the server on default port.
-
trigger API-routes over
curl
:make message='{"message": "waiting for the summer", "tags": ["random"] }' post-save make id=0 get-id make tag=random list-tag make toggle-logs
The rest of the article is tutorial explaining the application of Handle pattern in Haskell to build web-apps.
Also we can run stress testing for our server.
For that we need to install k6
and see
the README.md
for docs on how to run them in the directory test-stress
of this repo (note that it features it's own Makefile).
The library src/
defines types, interfaces, server and handlers
Types
- types of the domainError
- custom server errorsApi
- API for the appServer
- servant server and main environment (state) of the serviceDI.[Log | Time | Setup]
- interfaces for the app and common functionsServer.[Save | GetMessage | ListTag | ToggleLog]
- handlers of the API-routes
Executable app/
implements interfaces initialises service state and launches the app.
Main
- init and launch serverConfig
- read server configs from command line argumentsApp.DI.[DB | Log | Time | Setup]
- implement interfacesApp.State
- mutable state of the appApp.DI.Db.MockDb
- mock db, should be in separate package but kept here for simplicity
In this example and tutorial we will learn how to build flexible web-apps with the help of handle pattern. We will mention some key-factors of the web-development domain, discuss the problems and solutions and look at how to combine Handle pattern with servant. While implementing a small app.
What we will learn:
-
How to organise application with collection of interfaces
-
we can keep interface for external services separate to mock them for testing or swap implementations
-
to be flexible we propose API-route or user action first design for interfaces. It's better to build interfaces not from the point of view of the actual external service but from the user perspective of the app. From what our app wants form that external service in the given API-route.
-
the web-app domain is ocean wave not a solid ground to build castles. So we need to build with presence of uncertainty and unexpected changes in mind. Which corresponds badly with mathematical thinking. We need to use more flexible solutions.
-
How to hide mutable state with interfaces
In this app we use the Handle pattern for DI's But our version of Handle pattern is reversed in terms of where interfaces originate. In original Handle pattern we wrap external services with concrete interfaces. So interface is driven by external dependency. But I'd like to stress the point of user or app driven interfaces. We build small interfaces that are dedicated to concrete part of the app and use it locally. And on level of the executable we use concrete implementation.
This approach is inspired by the book Domain Modeling Made Functional by Scott Walschin (it uses F#). In this book it's well described how to build small and focused interfaces. I'd like to thank the Scott Walschin for providing this simple yet powerful technique to mitigate complexity. In this tutorial I'd like to adapt it to Haskell and building web apps with Reader pattern.
The history of this project is fun road of simplification of the Reader pattern to the bare essentials which eventually lead me to the question: Do we really need Reader in the first place?
So here is my progress:
type App env a = ReaderT env (ExceptT IO) a
newtype App env a = App (ReaderT env (ExceptT IO) a) deriving newtype (...)
newtype App env a = App (ReaderT env IO a)
- split env to local per API-route interfaces
- find out that mutable state can be hidden with interfaces also
- but if everything is described in interfaces, do we really need
ReaderT
at all? - handle pattern:
interfaces -> request -> IO response
During rewrite of the library code the library code size reduced from 266 to 210 LOC.
I've remove all dependencies on mtl, exceptions, liftIO
and etc. from the code.
This is an open question to me. But I'm inclined to think that no, we don't need the reader for our next web-application.
So let's dive in.
The Handle pattern is a very simple way to make dependency injection (DI) in Haskell. We express interfaces as plain records of functions and pass them around as arguments. So we notice that DI in Haskell is just a currying.
Why do we go with records and not with type classes? Because they are more flexible. We can store them as a value, pass to the function, transform with the function, keep in collection and even in mutable refs and they are not tied to particular type that is instance of the type class.
Using records can be somewhat cumbersome with polymorphic functions as generic parameters
should become parameters of the interface. But it turns out that this generic behavior is
rarely needed in web-apps so we can stick with plain IO
and handling errors with Either
or Maybe
.
There is alternative way to pass interfaces with Reader-pattern. Reader pattern encapsulates the collection of interfaces into environment. But this approach can lead to frustration that we need to have uniform collection of interfaces for all functions. And also we have some tiny performance overhead.
In contrast with currying we have no performance penalty and we can pass different flavours interfaces on the spot. This comes at the price of being self-repetitive as we don't use direct calls to external dependencies but call them over interface. This is why we have DI-in the first place.
Pros of the Handle-pattern
- easy to implement and reason about (as simple as applying argument to the function)
- very fast and efficient
- light-weight on dependencies and extensions
Let's start with the notion of interface as a record. Let's look at the examples. Here is the logger interface:
data Log = Log
{ logInfo :: Text -> IO ()
, logError :: Text -> IO ()
, logDebug :: Text -> IO ()
}
Here is an interface for the persistent storage:
data Db = Db
{ getMessage :: MessageId -> IO (Maybe Message)
, saveMessage :: Message -> IO MessageId
}
And instead of directly calling those functions we pass them around to functions that need them:
doSomethingWithStorage :: Db -> Message -> IO ()
doSomethingWithStorage Db{..} msg = ... use interface ...
We can use RecordWildCards
extension to bring all functions of the interface in
scope of the function. This our import
for interfaces.
Also interfaces can be organised in groups:
data Env = Env
{ db :: Db
, log :: Log
}
So if we want to do something with storage and log results as we go we can use this combined interface:
logAndStore :: Env -> a -> IO b
logAndStore (Env Db{..} Log{..}) a = ...
We can also have collection of interfaces. Imagine that we have a list of competing http-services that can be identified by name and all of them can be wrapped in the some interface.
We can create a map of interfaces:
type Services = Map ServiceName ServiceInterface
We can do some fun with it like querying a method concurrently and returning which ever returns first. Or iterating over all of them and returning result in the list. This all stems from the benefit of having interfaces as plain values.
For the web application we will pass the interface record to the handler of the API-route as first argument:
handler :: Interface -> ApiRequest -> IO ApiResponce
I used to extensively apply the Reader-pattern to keep the track of internal state in TVars
.
But it's interesting to note that with some discipline mutable variable management can also
be organised in interfaces.
In our app we have mutable shared state of logger verbosity we can update it by calling a API-method
toggle-logs
. It turns out that instead of using TVar
directly we can pass it
to interface initialization functions and internally if they rely on IO and usually they do
as we express external services with interfaces. They can read those variables
and behave according to changes.
So instead of doing this:
data Env = Env
{ isVerbose :: TVar Bool
, log :: Log
}
We can pass the TVar
to the initialization of the logger:
initLog :: TVar Bool -> IO Log
initLog =
The function initLog
hides dependency on mutable interface in the logger.
And we have to face the reality of isVerbose
mutable TVar
-state.
But what if it was also an interface?
We can provide an interface to tweak the logger state:
-- | Interface for tweaking configs
data Setup = Setup
{ toggleLogs :: IO ()
}
And in in the executable code we can initialise TVar
and
pass it to interface constructors for Log
and Setup
initEnv :: IO Env
initEnv = do
verboseVar <- newVerboseVar
setup <- initSetup verboseVar
log <- initLog verboseVar
pure $ Env setup log
Note that Setup
is not a data structure it's an interface
to trigger changes in the configs of the system.
And we share the link between logger and config only inside the executable app
.
On the level of the library they look decoupled.
This way we are not forced to chose TVar
between some other method of
sharing mutable state. It's all hided from the library.
By the Env
we only see the list of available actions
that can be performed on the app in terms of interfaces.
Keeping mutable variables visible to the user is more flexible approach as it allows for mutual-recursive dependencies. But for keeping them in interfaces graph should be acyclic. If your application does not need cyclic dependencies of interfaces. And I'm sure you don't want that to happen. We can turn mutable variables to interfaces and this matches nicely with Handle-pattern as everything becomes just a collection of interfaces.
For the previous example instead of passing TVar Bool
directly
to initialization functions we can create a wrapper that hides
away internal details of that mutable variable. We do that in the module app/App/State.hs
:
-- | Application mutable state
module App.State
( VerboseVar
, newVerboseVar
, isVerbose
, toggleVerbose
) where
import Control.Concurrent.STM
newtype VerboseVar = VerboseVar (TVar Bool)
newVerboseVar :: IO VerboseVar
newVerboseVar = VerboseVar <$> newTVarIO True
isVerbose :: VerboseVar -> IO Bool
isVerbose (VerboseVar var) = readTVarIO var
toggleVerbose :: VerboseVar -> IO ()
toggleVerbose (VerboseVar var) = atomically $ modifyTVar' var not
We define a newtype
wrapper for the mutable variable that controls
verbosity of the logs and provide several functions with which we can read that variable and set it up.
And signatures for our initialization functions become more self-explanatory:
initLog :: VerboseVar -> IO Log
initSetup :: VerboseVar -> Setup
You can look at the code how it's organised in the implementation of Log
and Setup
in the app/App/DI
modules.
In the servant we need to wrap response to Servant.Handler monad.
The main idea of the approach is to use servant only on top-level Server-method
and inside handlers we should work only in terms of the IO
and interfaces
that are passed as arguments to the handlers.
The cool thing about servant that it supports building handlers
not only with builtin monad Servant.Handler
but also with other
monads that can be converted to it.
So we can build server with plain IO
:
-- | Service interfaces by methods
data Env = Env
{ save :: Save.Env
, getMessage :: GetMessage.Env
, listTag :: ListTag.Env
, toggleLogs :: ToggleLog.Env
}
-- | Servant server for the app
server :: Env -> ServerT Api IO
server env =
Save.handle env.save
:<|> GetMessage.handle env.getMessage
:<|> ListTag.handle env.listTag
:<|> ToggleLog.handle env.toggleLogs
And in the app
to launch the server we use serveWithContextT
with our
custom lifting of IO
to Handler
monad:
import Control.Exception (try)
import Control.Monad.Except (ExceptT(..))
...
run config.port $ serveWithContextT (Proxy :: Proxy Api) EmptyContext toHandler $ server env
where
toHandler :: IO resp -> Servant.Handler resp
toHandler = Handler . ExceptT . try
For simplicity we use plain Text
error messages but in real app
we should define more fine grained type for ApiError
that we can convert
to servant errors.
Also as we work with plain IO
-monad we need to convert our
exceptions to servant ones so that they can be handled properly.
For that we use custom throwApi
function:
import Control.Exception (throwIO)
import Data.ByteString.Lazy qualified as BL
import Data.Text.Encoding qualified as Text
...
throwApi :: ApiError -> IO a
throwApi = throwIO . toServantError
where
toServantError (ApiError err) = err400 { errBody = BL.fromStrict $ Text.encodeUtf8 err }
In this example we keep all interfaces separate from the implementation.
And separation is on package level. Implementation is defined in the executable app
and interfaces are declared in the library src
.
This way we can facilitate top-down approach and work in terms of interfaces that are yet to be implemented. So if we zoom in building of the library:
stack build handler-proto:lib
We are not tied to concrete implementation and can quickly invent new interfaces and try them out.
I'd like to mention how easy it's to adapt our interfaces. As they are expressed as plain functions in the records. Let's consider logging example. We need to define the logging context dedicated to specific route. For example we need to prefix the logs with the name of the route.
We can adapt the whole logging interface by plugging the function:
mapLog :: (Text -> Text) -> Log -> Log
mapLog go logger = Log
{ logInfo = logger.logInfo . go
, logDebug = logger.logDebug . go
, logError = logger.logError . go
}
addLogContext :: Text -> Log -> Log
addLogContext contextMesage =
mapLog (mappend (contextMesage <> ": "))
In this example we use sort of logging middleware that inserts text-processing function prior to user call. We can transform the whole interface with it. And we can use it in the code by passing the logger to concrete API-route:
saveEnv =
Save.Env
{ db = env.db.save
, time = env.time
, log = addLogContext "api.save" env.log
}
listTagEnv =
ListTag.Env
{ db = env.db.listTag
, log = addLogContext "api.list-tag" env.log
}
Here we transform the common logger defined in top-level environment state of the service reader and pass it to the local loggers. And all local loggers will have this modified logging built into it.
I'd like to avoid having one big Env
record type that declares all possible
interfaces of the server. Instead of this it's much better
to have local small environments and interfaces that are dedicated to concrete part
of the app.
This example app is tiny. But for full-blown application one single Env
can become
a source of compilation-time pain very quickly.
Because if every handler will depend on it every additional feature will
force recompile everything scenario. And with time it would be very hard to
be able to reason about gigantic Env
. This will lead to reduce our time to market
as app is strongly coupled on one type of Env
and there will be many interdependencies
and compilation time will be bad.
Instead of this I prefer to keep Env
dedicated to methods.
Let's take a look at the interface of the ListTag
API-method.
In the task it returns the message by Tag
. Here is complete definition:
-- | Get by tag handler
module Server.ListTag
( Env(..)
, Db(..)
, handle
) where
import DI.Log
import Types
data Env = Env
{ db :: Db
, log :: Log
}
data Db = Db
{ listTag :: Tag -> IO [Message]
}
-----------------------------------------
-- Handler
handle :: Env -> Tag -> IO [Message]
handle (Env Db{..} Log{..}) tag = do
logInfo $ "list tag call: " <> display tag
listTag tag
Let's take it apart. It has it's own Env
:
data Env = Env
{ db :: Db
, log :: Log
}
And we can see that DB
-interface is also local to the method:
data Db = Db
{ listTag :: Tag -> IO [Message]
}
And the handler is defined in terms of local reader:
handle :: Env -> Tag -> IO [Message]
By imports we can see that it depends on common Types
and common logger interface:
import DI.Log
import Types
So with this approach we don't rely on Servant or on big one-for-all Env
.
We keep it small, simple and local to the method.
It would be painless to make a micro-service out of it.
But how we use it in the service? In the service we have that big Env
.
It contains all environments for the methods:
-- | Service environment by methods
data Env = Env
{ save :: Save.Env
, getMessage :: GetMessage.Env
, listTag :: ListTag.Env
, toggleLogs :: ToggleLog.Env
}
server :: Env -> ServerT Api IO
server env =
Save.handle env.save
:<|> GetMessage.handle env.listTag
Here we instantiate concrete set of interfaces for the API-method.
This way by local environment definition we can see which dependencies are used.
For example the ToggleLog
method uses only Log
and Setup
services
and modification of DB
or Time
interface will not affect it:
module Server.ToggleLog where
data Env = Env
{ log :: Log
, setup :: Setup
}
handle :: Env -> IO ()
Let's consider some benefits of this approach.
With this approach we define interfaces in terms of the method domain and we use only that much from the external dependency as we need to implement by the user action.
This can save us lots of trouble by trying to define beautiful and shiny DB-interface that will fit every needs. With single DB-interface to rule them all it can lead to disaster of bloated interfaces that are hard to modify and reason about. And they usually trigger recompilation of the whole project.
I argue that writing methods in this style we can keep our changes local.
Let's consider two types of changes:
- adding new method to existing interface
- adding new type of external dependency
For example if we want to add validTag
to our DB-interface
for the API-route ListTag
. We can add it to the local DB-interface:
data Db = Db
{ listTag :: Tag -> IO [Message]
, validTag :: Tag -> IO Bool
}
And with this approach we re-compile only two modules (this one and server that puts it all together) and we have no errors on the level of the library. But we will have missing field in the DI-implementation. Which is easy to define with mock or we can compile on the library level with
stack build handler-proto:lib
For a while and keep implementing our feature in terms of interfaces.
Let's imagine that validTag
is provided not by DB
but by some http-client Foo
.
We have two options here to consider:
- is it well defined and settled interface like
Log
? - is it hard to define interface with many features like
DB
?
If it's well defined then we can declare it under DI
-umbrella and just import
it to the handler:
module Server.ListTag where
import DI.Foo
data Env = Env
{ db :: Db
, log :: Log
, foo :: Foo -- ^ new interface here
}
And in handler we can use it in the same way:
handle :: Env -> Tag -> IO [Message]
handle (Env Db{..} Log{..} Foo{..}) tag = do
-- use validTag from Foo
Note that main service Env
does not change at all with this change.
It only changes if we add a new API-route.
Also we pass it to the local environment for ListTag
to make it compile.
Again we get no errors on library level and we recompile only two modules if
Foo
is already defined in DI
.
In the second option if we decide that this is hard to settle down and vague interface
like Db
one. We create local version of the Foo
and keep it inside
the handler module:
module Server.ListTag where
data Env = Env
{ db :: Db
, log :: Log
, foo :: Foo -- ^ new interface here
}
data Foo = Foo
{ validTag :: Tag -> IO Bool
}
And that's it. We also recompile only two modules and get no errors on the library level.
In web-applications domains are very flexible and features are incoherent at best and come to life and death as fast as the market wants them. And nothing can be done about that. Our domain is ocean wave and it's hard to build castles on top of it.
But we are Haskellers. We are mathy people. We like beautiful solid Math interfaces.
Forget it. This approach can lead to disaster in the web-application domain. The interfaces starting solid and cool quickly become incoherent and bloated.
So instead of building rock solid, beautiful interfaces I propose to build local small interfaces that are easy to introduce and throw away if they are not needed. It makes us more flexible and easy to adapt to the changes.
Let's consider some downsides.
One downside that comes to mind is code duplication. Say we work as a team on new features. And our project uses small interfaces as this post advertised and we can build stuff in isolation and we are happy with that.
But say what if sub-team A working on route A wants some interfaces that are local to the route B that team B works on. What should we do?
Should we introduce cross dependencies or try to isolate or regroup interfaces in the DI
?
This is an open question. If we take this approach to extreme we should allow the code to
be duplicated. So the same local DB-method used in both cases should stay inside local version.
And in the DI
implementation stage we will just use the same low-level function
to instantiate it.
This code duplication I think is a price that worth it. As we still don't get artificial coupling. Because as I stated our domain is always in flux. As it evolves what looked the same on Monday might become not so the same on Friday next month. And with this coupling introduced we will bring the unwonted change to the interface that does not really need it. But as coupling was codified we will forget that we need to keep them separate and we will bring stronger bound that will prevent changes from being local and flexible.
I think that this is where software engineering stops to look like a Science and starts to look like like an Art. There is no right answer to this. We should balance it as we grow and our app grows. Some interfaces can be reused and we might want to solidify them to not to copy over and over. But some are real demons of change and it would be hard to keep them at bay. And we should use them localised per method.
So it becomes the matter of taste and intuition. But starting small with local ones I think it pays off and great decision for web-application building. As it's much more flexible approach.
Recommended way to organise service settings is with config file
that is easy to read for humans (for example YAML
or TOML
formats).
We can parse the YAML
with yaml
library and parse CLI-arguments with
optparse-applicative
library. The code example is in the app/Config.hs
.
For our app we can see the available options with:
stack run -- --help
We can transform our interfaces to augment the default implementation
with some new behavior. For example we can transform the DB-interface
to add logging for every call to DB. This approach is implemented in the branch
middlewares
.
Let's outline the idea.
Take for example the interface to get the message by it's id:
data Db = Db
{ getMessage :: MessageId -> IO (Maybe Message)
}
We can use logger to log every call to the interface:
dbLog :: Log -> Db -> Db
dbLog logger (Db getMessage) = Db getMessage'
where
Log{..} = addLogContext "db.getMessage"
getMessage' msgId = do
logInfo $ "Input message id: " <> display msgId
mRes <- getMessage msgId
case mRes of
Just res -> logInfo $ "Output message: " <> display res
Nothing -> logError $ "Failed to get message for id: " <> display msgId
pure mRes
We transform the Db-interface in a way that every call to getMessage
gets wrapped
with logging calls. After that transformation we can even omit the logger
from the dependency of the handler and we can just use transformed DB-interface.
We can apply transformation in the Main
function prior to launch of the app:
-- init local envirnoments
env =
Env
{
save = ...
getMessage =
let logGetMessage = addLogContext "api.get-message" ilog
in GetMessage.Env (GetMessage.dbLog logGetMessage idb.getMessage) logGetMessage
listTag = ...
toggleLogs = ...
}
And we can do the same with DB-interfaces for other methods. This example shows how we can add middleware behavior to our interfaces without changing concrete implementation of the interface. We use interface at the input as a black box and augment it with some additional behavior. In this case it was logging.
So we have defined our small app. But story does not end there. We have to implement new and new API-routes and features and app becomes not so small. How to keep it small nonetheless?
I think there is no answer to this. We have to balance on the waves. But in this section I'd like to mention some further steps.
For simplicity I kept all API definition in the single module Api
.
In real case we can split it also to modules as we did it with handlers.
This is a proper place not only for servant API definition but also for all
transport types that are used for response and requests. We should keep it
separate from domain types.
Also we can go down to micro-service route and split the app
by groups of logically related methods to separate services.
On this stage our method with local interfaces can pay off well.
As we already have separated environments we can define separate
apps with local Env
's becoming top-level ones.
But keep in mind the hidden dependencies of mutable state on the app initialization level.
It can also become tangled. I advise instead of direct usage of TVar
's to wrap
them to newtype
s and create the modules that provide meaningful interface
for them so that TVar
details are hidden. This way it's easier to decouple things
or see which one depends on which.
We have discussed a Handle pattern and how to use it to build flexible web-apps with servant that are easy to change and keep development with the wave.
Let's mention the points:
-
we can organise application with interfaces as records
-
with this approach we can decouple implementation from the servant details
-
we can use local environments for groups of interfaces for API-routes and assemble them in the last stage on level of the server definition or we can swap it to micro-service design.
-
local interfaces that are driven by the API-routes give more flexible solutions that are local to the compiling routine and more easy to think about at price of possible duplication.
-
the web-app domain is ocean wave not a solid ground to build castles. So we need to build with presence of uncertainty and unexpected changes in mind. Which corresponds badly with mathematical thinking. We need to use more flexible solutions.
-
Separation of interface definition and implementation on package level. Use executable (or separate package) for implementation and inside the library think in terms of open interfaces that are yet to be defined.
-
Mutable state can be hided behind interfaces completely. We can link internal dependencies in the initialisation step if interfaces want to communicate with each other.
Happy web-apps building with Haskell!