Proper way to treat global flags in Haskell

后端 未结 4 788
天涯浪人
天涯浪人 2021-02-07 12:19

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

相关标签:
4条回答
  • 2021-02-07 12:46

    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.

    0 讨论(0)
  • 2021-02-07 12:57

    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

    0 讨论(0)
  • 2021-02-07 13:09

    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.

    0 讨论(0)
  • 2021-02-07 13:12

    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.)

    0 讨论(0)
提交回复
热议问题