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; o
Another option is GHC implicit parameters. These give a less painful version of your option (2): intermediate type signatures get infected, but you don't have to change any intermediate code.
Here's an example:
{-# LANGUAGE ImplicitParams #-}
import System.Environment (getArgs)
-- Put the flags in a record so you can add new flags later
-- without affecting existing type signatures.
data Flags = Flags { flag :: Bool }
-- Leaf functions that read the flags need the implicit argument
-- constraint '(?flags::Flags)'. This is reasonable.
leafFunction :: (?flags::Flags) => String
leafFunction = if flag ?flags then "do_stuff_A" else "do_stuff_B"
-- Implicit argument constraints are propagated to callers, so
-- intermediate functions also need the implicit argument
-- constraint. This is annoying.
intermediateFunction :: (?flags::Flags) => String
intermediateFunction = "We are going to " ++ leafFunction
-- Implicit arguments can be bound at the top level, say after
-- parsing command line arguments or a configuration file.
main :: IO ()
main = do
-- Read the flag value from the command line.
commandLineFlag <- (read . head) `fmap` getArgs
-- Bind the implicit argument.
let ?flags = Flags { flag = commandLineFlag }
-- Subsequent code has access to the bound implicit.
print intermediateFunction
If you run this program with argument True
it prints We are going to do_stuff_A
; with argument False
it prints We are going to do_stuff_B
.
I think this approach is similar to the reflection package mentioned in another answer, and I think the HFlags mentioned in the accepted answer is probably a better choice, but I'm adding this answer for completeness.