I often need to make a core function that's used in many places somehow configurable - i.e., it may use either algorithm A or algorithm B depending on a command-line switch; or have it print extra-verbose information to stdout if a 'debug' flag is set somehow.
How should I implement such global flags?
I see 4 options, all of them aren't really good.
Read command-line arguments from the function - bad, because that needs IO monad and the core calculation functions all are pure, I don't want to get IO in there;
Pass a parameter from main/IO all the way through to the 'leaf' function which needs to change behavior - completely unusable, as that means changing a dozen of unrelated functions in different modules to pass this parameter, and I want to try out such configuration options multiple times without changing the wrapping code every time;
Use unsafePerformIO
to get a true global variable - feels ugly and overkill for such a simple issue;
Right in the middle of the function have code for both options and comment one of them out. Or have functions do_stuff_A and do_stuff_B, and change which one of them is called depending on what a global function needDebugInfo=True
says. That's what I'm doing now for the debuginfo
, but it can't be changed w/o recompile, and it shouldn't really be the best available way...
I don't need or want global mutable state - I want to have a simple global flag that's immutable at runtime but can be somehow set when the program is launched. Are there any options ?
These days, I prefer to use a Reader
monad to structure the read-only state of the application. The environment is initalized at startup, and then available throughout the top level of the program.
An example is xmonad:
newtype X a = X (ReaderT XConf (StateT XState IO) a)
deriving (Functor, Monad, MonadIO, MonadReader XConf)
The top level parts of the program run in X
instead of IO
; where XConf
is the data structure initalized by command line flags (and environment variables).
The XConf
state can then be passed as pure data to the functions that need it. With newtype deriving you also get to reuse all the MonadReader code for accessing state.
This approach retains the semantic purity of 2. but gives you less code to write, as the monad does the plumbing.
I think its the "true" Haskell way to do read-only config state.
--
The approaches that use unsafePerformIO
to initialize global state also work, of course, but tend to bite you eventually (e.g. when you make your program concurrent or parallel). They also have funny initialization semantics.
You can use the Reader
monad to get the same effect as passing a parameter around everywhere. Applicative style can make the overhead fairly low compared to normal functional code, but it can still be pretty awkward. This is the most common solution to the configuration problem, but I don't find it terribly satisfactory; indeed, passing the parameter around explicitly is often less ugly.
An alternative is the reflection package, which lets you pass around common configuration data like this around through typeclass contexts, which means none of your code has to change to plumb the additional value, only the types. Basically, you add a new type parameter to every input/result type in your program, so that everything operating within the context of a certain configuration has the type that corresponds to that configuration in its type. That type stops you accidentally mixing values using multiple configurations, and gives you access to the associated configuration at runtime.
This avoids the overhead of writing everything in applicative style, while still being safe, and allowing you to mix multiple configurations. It's a lot simpler than it sounds; here's an example.
(Full discloure: I've worked on the reflection package.)
Our new HFlags library is exactly for this.
If you would like to see an example usage like your example, look into this:
https://github.com/errge/hflags/blob/master/examples/ImportExample.hs
https://github.com/errge/hflags/blob/master/examples/X/B.hs
https://github.com/errge/hflags/blob/master/examples/X/Y_Y/A.hs
No kind of parameter passing is needed between the modules, and you can define new flags with an easy syntax. It uses unsafePerformIO internally, but we think that it does that in a safe way, and you won't have to concern yourself with that.
There is a blog post about this stuff at: http://blog.risko.hu/2012/04/ann-hflags-0.html
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With