Writing Configuration Files in Haskell
Configuration files are convenient. I don’t want to recompile my code to change the port number it’s using. Configuration files are also a nuisance. I have to pick some format that my language of choice supports, deal with it’s syntax, and probably write my own code to convert the values into something useful to my progam. I’m starting a new project using Haskell, so I took a look around to see if it had anything clever to deal with configuration files.
Hint is a Haskell libary for embedding a Haskell interpreter in a program. Using this library and a bit of extra code, you can write configuration files as Haskell code, and load them at runtime.
This method gives a few nice features. Since the configuration files are written in Haskell, they can be loaded in the interpreter and validated beforehand. Also, the configuration file can contain any standard Haskell constructs, so there is no need to parse strings.
Unfortunately, it is inconvenient to type-check the actual parameters. We’d need to have the source code that defines the configuration data-type around at runtime to do that.
So, the implementation:
For demonstration purposes, this program will load it’s configuration from a file specified in the command line, and print out the resulting configuration (or error messages):
main = do args <- getArgs
when (null args) (fail "Must pass configuration file as parameter")
config <- loadConfig (head args)
case config of
Left err -> fail ("Could not load configuration:" ++ show err)
Right c -> runApp c
runApp config = putStrLn . show $ config
The datatype the configuration will ultimately be stored in:
data Config = Config { cBaseUrl :: String
, cPort :: Maybe Int
}
deriving Show
The loadConfig function actually does the interesting work:
loadConfig file = runInterpreter $
The first thing we do is set the interpreter to use extended defaulting rules. Normally, if we did not provide a type annotation, Haskell would assume that a top level integer defintion is of type Integer. However, we actually want an Int. Turning on extended defaulting rules fixes this. Then we load the config file, set it to the top level, and set up some standard imports.
do set [languageExtensions := [ExtendedDefaultRules]]
loadModules [file]
getLoadedModules >>= setTopLevelModules
setImports ["Prelude"]
Now we are ready to actually try to load configuration parameters from the file.
params <- fmap (S.fromList . mapMaybe getFunction) $
getModuleExports "Main"
port <- maybeInterpret params "port" (as :: Int)
url <- interpret "baseUrl" (as :: String)
return $ Config url port
Because the port number is optional, we need some special handling. Hint doesn’t provide a want to specifically ask if a definition is available, so we have to get the top-level functions out of the list of module exports:
getFunction (Fun i) = Just i
getFunction _ = Nothing
Then, for optional parameters, we use maybeInterpret to check against the list and only try to read the parameter if it exists in the configuration file:
Set Id -> String -> a -> m (Maybe a)
maybeInterpret s t w = if S.member t s
then liftM Just $ interpret t w
else return Nothing
Then we can write a configuration file like:
port = 8080
baseUrl = "test"
Running our program with this configuration file gives the output:
Config {cBaseUrl = "test", cPort = Just 8080}
Full source code is here, and the configuration file is here
