Conferer, a configuration library for Haskell

Conferer is a library that helps you configure your Haskell applications in an ergonomic way.

Conferer, a configuration library for Haskell

Image by rawpixel.com

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!