To Dream of Magick

Dreamer Shaper Seeker Maker

Monadic application configuration

Posted on Fri Nov 29 00:00:00 UTC 2013

This is a very old article. I recommend instead referring to Configurating your Haskell Application

While the change is largely invisible, I recently rewrote the CMS for this website. That was actually unintentional... I was writing a "dashboard" application for my own personal use, and the code and structural commonalities were just too great to ignore.

As an application used in more than one place, though, configuration became more important than ever. My configuration object has become large and unweildy, but as it grew I started developing tools to handle loading the object from JSON.

I discovered that I actually have three classes of parameters in my configuration object, at least so far as loading them goes. The first class is those parameters that are required and have no reasonable defaults. The second class are parameters that are required, but have a reasonable default. The third class is those parameters that are not required, because their default is to disable an entire feature.

  • webRoot -- This is one parameter that is required. Yesod uses this to generate canonical URLs in the application. There is no way for Yesod to know where the application is actually hosted, so I put this into configuration so that I can generate localhost URLs when running the application locally, and savannidgerinel.com URLs when running the application on the server
  • port -- This parameter is required, but I am able to hard-code a default in the application. The traditional default for an application like this is 8000 or 8080, and you only need to change it if you want to post somewhere else.
  • siteTitle -- This provides a little title block at the top of every page in the template. If it is absent, the value is simply Nothing and the title block does not get generated at all. Optional, but no reasonable default.

I tried a variety of ways of handling this, and each one introduced problems with supporting all three features. Finally, I settled on using a State operation to build the configuration.

configBuilder :: JSON.JSObject JSON.JSValue -> State Configuration ()
setIf :: (val -> Configuration -> Configuration) -> JSON.Result JSON.JSValue -> State Configuration ()
set :: (val -> Configuration -> Configuration) -> JSON.Result JSON.JSValue -> State Configuration ()

I have a trio of functions here. In my final implementation, set and setIf are actually defined only in configBuilder. Both are pretty simple. set will set a value to the configuration if the value is a JSON.Ok value. If it is JSON.Error, it will simply call error. This terminates the application, but with an error message that at least points towards the incorrect or missing value.

setIf is almost identical to set, except that it simply returns if the value was absent or could not be read.

let setIf updater (JSON.Error _) = return ()
    setIf updater val@(JSON.Ok _) = set updater val
    set updater (JSON.Error err) = error err
    set updater (JSON.Ok val) = get >>= put . updater val

set consists of three steps. The first one gets the Configuration object from the State context. The second applies the updater function to update the Configuration. Finally, the resulting configuration is put back into the context. The function could be rewritten like so:

set updater (JSON.Ok val) = do
    config <- get
    put (updater val config)

Before I go on, understand that State Configuration () basically means Configuration -> ((), Configuration). Any function whose result is State s v is actually still a function which requires an input parameter of type s (your running state) and will return a tuple of type (v, s) (the result of the operation as well as the new state).

The declaration of State is

type State state value = StateT state Identity value
newtype StateT state monad value = StateT { runStateT :: state -> monad (value, state) }

value in the above declarations always refers to the value that results from one state transformation, but not the new state itself.

The monadic declaration for State means that within a State context, you can run one state operation after another and each one will have access (invisibly) to the "global" state. Now, admittedly, it is not totally invisible. My set operations call the get operation and the put operation in order to alter the state, and this will be the case for any function which directly accesses the state.

So, if we consider Configuration to be the state that we care about, configBuilder starts to look like this:

JSON.valFromObj :: JSON.JSON v => String -> JSON.JSObject JSON.JSValue -> Result v

configBuilder obj = do
    setIf (\v c -> c {port=v}) (JSON.valFromObj "port" obj)
    set   (\v c -> c {webRoot=v}) (JSON.valFromObj "webRoot" obj)

I actually have about ten such commands in my configuration object. For both set and setIf, the first parameter is the function that converts a value and a configuration into a configuration update. I have to state this explicitely because you cannot simply pass a configuration field as a parameter if you intend to set the field. {port=x} is a special syntax that is not passable, whereas port is. But port by itself is a getter function, and {port=x} is a sort of a setting construct.

The second parameter is the result of trying to get a value from a JSON dictionary value. valFromObj, by returning a value that must already have a JSON declaration, automatically parses the JSValue into whatever data type is needed, and returns a failure if either the field is absent or the value cannot be parsed. The type inference system is pretty magnificent for making this magic work smoothly.