Chapter Three: Ambient Config
Often things like configuration variables read in from the command line or a file, shared database pools, or functions that we want to use in our application at runtime but swapped out for development or at test-time (i.e. dependency injection).
Haskell is a "pure" language where everything is explicit. As a naive approach, this can sometimes lead to boilerplate function arguments for threading a value around your application. This also means many changes when something breaks. To get around this, we sometimes want to have something available "ambiently" in an application, as if it were a global constant, and have all that threading done for us by helper functions.
This is a standard pattern known as a
Reader
. It's so common that the Haskell community has started rapidly embracing it as the wrapper for applications. You can have global and local Reader
s, but we'll be focusing here on the global one for our application.The first part of this is to create a data structure that will contain our global context. Many people call this
Env
, but that can conflict with standard terminology for environment variables, which environment an application is running in (test, development, staging, production), and so on. We have opted to call this Config
.Fission.Config.Types
data Config = Config
{ _logFunc :: !LogFunc
, _host :: !Web.Host
, _dbPath :: !DB.Path
, _dbPool :: !DB.Pool
-- and so on
}
makeLenses ''Config
We may move to SuperRecord in the future, for even less boilerplate plus some nifty additional super powers 🦸
Keeping your Config as flat as possible is generally a good idea. While many people have an intuition that nesting things by concept (e.g. database) is a good idea, it's generally more trouble than it's worth in practice.
While we have all of this threading done for us, we still want to know which part of the config is required by which part of the application. This approach has a few upsides:
- 1.Easy to read labels help you keep track of what a function can access
- 2.Functions don't depend on specific concrete
Config
s, just fields - 3.The compiler can help you refactor if change the
Config
- 4.Constraints bubble up to callers, so dependencies can't hide
Here's an example that retrieves the web host name, and combines it with a ncie message that is passed in as an argument:
hostMsg :: MonadRIO cfg m
=> Has Web.Host cfg
=> Text
-> m Text
hostMsg greeting = do
Web.Host hostname <- Config.get
return $ greeting <> ", the app is live at " <> hostname
Config.get
pulls out a value from the Config
. It knows which value you want because of the expected constructor (Web.Host
) on the left side of the <-
.MonadRIO
is a constraint defined in our application. It's an alias for the very common scenario in this style of wanting both MonadIO m
and MonadReader cfg m
. Our prelude (
RIO
) depends on having functions available ambiently in this way. One common case is with logging, which needs a LogFunc
ready for use. logHost :: MonadRIO cfg m
=> Has Web.Host cfg
=> HasLogFunc cfg
-> m ()
logHost = logInfo $ "Host name is " <> display hostname
Unlike the first example, the constraint has no space after the
Has
. This is because we're using the Has library to clean up some of the boilerplate associated with creating so many constraints.You are likely to want to add custom fields to the
Config
record. The first step is to ensure that the type is unique to the application, wrapping common types in newtype
:Fission.Web.Types
newtype Port = Port { getPort :: Int }
deriving Show
Next, add it to the
Config
itself:Fission.Config.Types
data Config = Config
{ _logFunc :: !LogFunc
, _host :: !Web.Host
, _port :: !Web.Port -- THIS LINE
, _dbPath :: !DB.Path
, _dbPool :: !DB.Pool
}
makeLenses ''Config
Because of the
makeLenses
declaration, you automagically get a lens (superpowered accessor) for the new field called port
instance Has Web.Port Config where
hasLens = port
That's it! It's available everywhere in the application now!
logPort :: MonadRIO cfg m
=> Has Web.Port cfg
=> HasLogFunc cfg
=> m ()
logPort = do
Web.Port port <- Config.get
logInfo $ displayShow port
Last modified 3yr ago