Conferer, una biblioteca de configuración para Haskell

Conferer es una biblioteca que ayuda a configurar tus aplicaciones en Haskell con una interfaz ergónomica.

Conferer, una biblioteca de configuración para Haskell

If want to see this post in English, click here.

Los desafíos de configurar aplicaciones en Haskell

En toda aplicación suficientemente grande llega el momento en el que hay que manejar configuración. Si querés saber más sobre por que querrías configuración en tus proyectos te invitamos a leer este post.

En particular en Haskell, algunas bibliotecas (hspec, snap, etc) proveen funciones para obtener configuración ya sea a través de env vars, de un archivo o de otra manera. Sin embargo, cada una lo hace de una forma diferente (y otras ni siquiera proveen esto) así que si querés obtener toda la configuración para tu programa de un solo lugar vas a tener que programarlo a mano.

Esto implica que cada vez que implementés la configuracion en una aplicación nueva vas a tener que resolver los mismos problemas una y otra vez: manejo de errores, de valores por defecto, de diferentes ambientes, el parseo de la configuración en valores de tu programa, etc.

Una solución: Conferer

Conferer es una biblioteca que ayuda a configurar tus aplicaciones en Haskell con una interfaz ergónomica.

Esto lo logra:

  • Dejándote definir diferentes fuentes de donde obtener la configuración.
  • Resolviendo el parseo de configuración en valores válidos de tu programa.
  • Permitiéndote configurar valores por defecto.

Ejemplo

En este ejemplo vamos a ver a alto nivel varias de las funcionalidades de Conferer.

Digamos que queremos crear un servidor HTTP al cual le queremos especificar tanto el puerto como un mensaje inicial que se va a mostrar cada vez que lo arranquemos.

Definiendo de donde leemos la configuración

Lo primero que necesitamos es definir de donde va a venir la configuración. Conferer nos permite hacer esto de manera declarativa, lo único que hay que hacer es listar las fuentes.

En el siguiente código, definimos una configuración que lee de la línea de comandos, de variables de ambiente y de un archivo Dhall llamado "config.dhall".

mkMyConfig :: IO Config
mkMyConfig = mkConfig' []
  [ Cli.fromConfig
  , Env.fromConfig "myapp"
  , Dhall.fromFilePath "config.dhall"
  ]

Cuando intentemos obtener un valor de la configuración se va a intentar buscar en las fuentes en el orden que están listadas, por lo que también estamos declarando su prioridad en la lista. En este ejemplo, si quisiesemos sobre-escribir algún valor del archivo de Dhall podríamos hacerlo pasando ese valor como un parámetro de línea de comandos.

Interpretando la configuración

La typeclass FromConfig es usada cuando pedimos valores de nuestra configuración, por lo que vamos a necesitar una instancia de la misma para los tipos de todo lo que queramos que sea configurable. Conferer ya viene con algunas instancias de FromConfig para tipos comunes como String, Int, Bool, etc. Y si nuestro tipo tiene una instancia de Generic podemos derivar la instancia de FromConfig así:

data AppConfig = AppConfig
  { appConfigServer :: Warp.Settings
  , appConfigBanner :: String
  } deriving (Generic)

instance Conferer.FromConfig AppConfig

Además, se pueden agregar nuevas instancias de FromConfig para configuraciones de bibliotecas comunes. De hecho, algunas ya están provistas en diferentes paquetes (conferer-warp, conferer-hedis, etc).

Configurando valores por defecto

No queremos que el usuario tenga que configurar absolutamente todo.

Podemos proveer valores por defecto para cada valor de la configuración definiendo una instancia de DefaultConfig para el tipo de nuestra configuración.

instance Conferer.DefaultConfig AppConfig where
  configDef = AppConfig
    { appConfigServer = Conferer.configDef
    , appConfigBanner = "Hi conferer"
    }

Esto le permite al usuario pasar solo algunos de los parámetros y dejar que los valores por defecto se encarguen del resto. Por ejemplo, si corriésemos nuestro programa así:

$ ./conferer-example --server.port=2222

se van a usar todos los ajustes por defecto del server de warp excepto el puerto que es lo que pasamos explícitamente.

Detectando cuando las cosas salen mal con mensajes de error

Si la configuración provista es invalida, Conferer va a fallar con un error descriptivo tan pronto como trate de procesarla.

$ ./conferer-example --server.port=fewa
conferer-example: Couldn't parse value 'fewa' from key '"server.port"' as Int

Usando los valores configurados

Una vez definidas las fuentes, implementados los FromConfigs necesarios y elegidos los defaults, es momento de usar los valores que Conferer obtiene:

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

Este ejemplo muestra como una vez obtenidos los valores de la configuración usando Conferer se puede escribir lógica del negocio que sea agnóstica a de donde salieron esos valores.

Conclusión

En este ejemplo construimos un programa donde el manejo de la configuración no se mete en el medio del código de dominio que queremos escribir.

Adicionalmente, tanto las fuentes como las instancias de FromConfig pueden ser implementadas como paquetes independientes, permitiendo que cualquiera extienda Conferer.

Un ejemplo completo del código mostrado se puede encontrar acá. También, si querés un tutorial guíado, podés revisar esto.

Para cerrar

Ya estamos usando Conferer en algunas aplicaciones internas en 10Pines y hasta ahora demostró ser útil para facilitar la configuración de nuestras aplicaciones escritas en Haskell.

Hoy estamos anunciando la primer versión estable y planeamos seguir trabjando en mejoras, por lo que cualquier comentario o sugerencia es bienvenido.

Saludos!