Conferer, a configuration library for Haskell
Conferer is a library that helps you configure your Haskell applications in an ergonomic way.
Si querés leer este post en español, clickeá acá.
Challenges of configuring Haskell applications
In any big enough application, you’ll eventually need to handle configuration. You can read about why you would want configuration in your apps here.
In Haskell in particular, some libraries (hspec, snap, etc.) provide functions to fetch the configuration, be it through env vars, from a config file, etc. However, each one of them does it in a different way (and some don’t do it at all) so if you wanted to have all the needed configuration in one place, you’d need to write that yourself.
This means solving some of the same problems over and over again, like handling defaults, managing different environments, error handling (e.g. in case of missing or invalid parameters), parsing the configuration into values in your program, etc.
A solution: Conferer
Conferer is a library that helps you configure your Haskell applications in an ergonomic way.
It does this by:
- Letting you define different sources to get the config from.
- Handling the parsing of the config into the values you use in your program.
- Allowing you to set default values.
Example
This example is a high-level overview of several Conferer’s features. Let’s say we want to create a simple HTTP server where we’d like the user to be able to specify the port and the banner that will be displayed upon startup.
Defining where we read the configuration from
The first thing we need to do is defining where we’ll read the configuration from. Conferer lets us do this in a declarative manner by listing sources.
In the following snippet, we define a configuration that is read from CLI params, environment variables and a dhall file named "config.dhall".
mkMyConfig :: IO Config
mkMyConfig = mkConfig' []
[ Cli.fromConfig
, Env.fromConfig "myapp"
, Dhall.fromFilePath "config.dhall"
]
When we try to get a value from the config, the sources are checked in the order they were listed, which means we are also declaring their priority here. In this example, if we wanted to override some value from the Dhall config file we can achieve that by passing that value as a CLI parameter.
Interpreting the configuration
The FromConfig
typeclass is used when we try to fetch a value from our config, so we’ll need an instance of it for the types of all the values we want to configure. Conferer already has instances of FromConfig
for a lot of common types like String
, Int
, Bool
, etc. And if our type has an instance of Generic
, we can just automatically derive the FromConfig
instance:
data AppConfig = AppConfig
{ appConfigServer :: Warp.Settings
, appConfigBanner :: String
} deriving (Generic)
instance Conferer.FromConfig AppConfig
Also, it’s not hard to add new instances of FromConfig
for settings of commonly used libraries and many are already provided in separate packages (conferer-warp
, conferer-hedis
, etc).
Setting defaults
We don’t want the user to always have to configure everything explicitly.
We can provide default values for each value in the configuration by defining a DefaultConfig
instance for our type.
instance Conferer.DefaultConfig AppConfig where
configDef = AppConfig
{ appConfigServer = Conferer.configDef
, appConfigBanner = "Hi conferer"
}
This also allows the user to partially configure a value and have the rest be filled with the default. For example, running our program like:
$ ./conferer-example --server.port=2222
will use all the default settings for the warp server except the port that is being overridden by the parameter we passed.
Detecting when things are wrong with error messages
If the provided configuration is invalid, Conferer will fail with a descriptive error message as soon as it tries to consume it.
$ ./conferer-example --server.port=fewa
conferer-example: Couldn't parse value 'fewa' from key '"server.port"' as Int
Using the configured values
Now that we have defined the sources, how the config is parsed and chosen the defaults, it’s time we use the values Conferer provides us:
main :: IO ()
main = do
config <- mkMyConfig
appConfig <- Conferer.fetch config
let warpSettings = appConfigServer appConfig
putStrLn $ appConfigBanner appConfig
putStrLn $ "Running on port: " ++ show (Warp.getPort $ warpSettings)
Warp.runSettings warpSettings application
This shows how, once you fetch the configuration values using Conferer, you can just write your business logic being agnostic to where those values came from.
Conclusion
In this example, we’ve built a program where handling configuration doesn’t get in the way of the interesting logic we want to write.
Additionally, both the sources and the FromConfig
instances can be implemented in independent packages, allowing anyone to extend Conferer.
The full example code can be found here. Also, if you want to check a guided tutorial, you can check this.
Wrapping up
We’ve been using Conferer in some in-house applications at 10Pines and so far it’s been useful to ease the configuration of our applications written in Haskell.
Today we are announcing the first stable version and we plan to continue working on improvements, so any feedback is welcome.
Saludos!