.github/workflows | ||
src | ||
test | ||
.gitignore | ||
CHANGELOG.md | ||
configurant.cabal | ||
hie.yaml | ||
LICENSE | ||
README.md |
configurant
figurant, noun: an actor that figures in a scene without speaking or without taking a prominent part.
Configuring your application based on environment variables should be easy. configurant
, a clone of Kelsey Hightower's envconfig
, allows a user to populate a given data structure automatically by reading the environment variable specified in a given type. It aims to prioritize concision, interface simplicity, and error messages.
Example
It's easier to show then tell. Given a Server
type containing configuration parameters for some hypothetical web server:
{-# LANGUAGE DeriveGeneric, ImportQualifiedPost, OverloadedLabels, OverloadedStrings #-}
import Configurant (Config, (!))
import Configurant qualified as Config
import GHC.Generics (Generic)
data Server = Server
{ port :: Int,
hostname :: String
} deriving (Show, Generic)
you can construct a description of how to parse its environment variables, using the OverloadedLabels
extension and !
syntax from the named
package:
serverEnv :: Config Server
serverEnv = Config.record
! #port (Config.read "SERVER_PORT")
! #hostname (Config.string "SERVER_HOSTNAME")
You can then use Config.fromEnv
to construct a Server
object based on the environment variables with which the program was invoked:
main :: IO ()
main = Config.fromEnv serverEnv >>= print
There exists the Config.fromEnvOrExit
helper to handle the common case of printing errors to stdout and then exiting.
How does this work?
In Go, it's trivial to add metadata to fields of a structure thanks to struct tags. Haskell doesn't support such a feature natively, but we can embed it in the type system without too much trouble. The higgledy
package allows us to treat our Server
type as though it were a higher-kinded type, of kind (Type -> Type) -> Type
. In other words, this representation has a shape functor that wraps its fields, as though we had declared a type that looked like this:
data ServerF f = ServerF
{ port :: f Int
, hostname :: f Int
} deriving (Show, Generic)
With this higher-kinded representation, it's fairly trivial to slot in a validation applicative as the f
parameter, which allows us to specify validators for individual fields. All the details of this higher-kinded representation are abstracted away in this library; if you use the !
syntax and overloaded labels, you should never have to worry about it.
Thanks
Kelsey Hightower's envconfig
served as the primary source of inspiration. The idea to use higgledy
came from me finding harg
, though I didn't look at its implementation: harg
does much more than configurant
does but has a larger dependency footprint.