在Haskell中处理全局标志的正确方法

时间:2012-04-23 17:40:26

标签: haskell command-line-arguments

我经常需要制作一个在某些地方以某种方式可配置使用的核心功能 - 即,它可以使用算法A或算法B,具体取决于命令行开关;如果以某种方式设置'debug'标志,或者让它向stdout打印超详细信息。

我应该如何实现这样的全局标志?

我看到4个选项,所有选项都不是很好。

  1. 从函数中读取命令行参数 - 坏,因为需要IO monad并且核心计算函数都是纯的,我不想在那里得到IO;

  2. 将参数从main / IO一直传递到需要更改行为的'leaf'函数 - 完全无法使用,因为这意味着更改不同模块中的十几个不相关的函数以传递此参数,并且我想多次尝试这样的配置选项,而不是每次都更改包装代码;

  3. 使用unsafePerformIO获取真正的全局变量 - 对于这样一个简单的问题感到丑陋和过分;

  4. 在函数中间右侧有两个选项的代码,并将其中一个注释掉。或者使用函数do_stuff_A和do_stuff_B,并根据全局函数needDebugInfo=True的说法更改调用哪一个函数。这就是我现在正在为debuginfo做的事情,但它无法通过重新编译来改变,它不应该是最好的方式...

  5. 我不需要或想要全局可变状态 - 我希望有一个简单的全局标志,它在运行时是不可变的,但可以在程序启动时以某种方式设置。有什么选择吗?

4 个答案:

答案 0 :(得分:16)

现在,我更喜欢使用a Reader monad来构建应用程序的只读状态。环境在启动时初始化,然后在整个程序的顶层提供。

示例is xmonad

newtype X a = X (ReaderT XConf (StateT XState IO) a)
    deriving (Functor, Monad, MonadIO, MonadReader XConf)

该计划的顶级部分在X而不是IO中运行;其中XConf是由命令行标志(和环境变量)初始化的数据结构。

然后可以将XConf状态作为纯数据传递给需要它的函数。使用newtype派生,您还可以重用所有MonadReader代码来访问状态。

这种方法保留了2的语义纯度,但是你可以用更少的代码来编写代码,就像monad做管道一样。

我认为它是“真正的”Haskell做只读配置状态的方法。

-

使用unsafePerformIO来初始化全局状态的方法当然也可以工作,但最终会让你感到困惑(例如,当你使程序同时或并行时)。他们也有funny initialization semantics

答案 1 :(得分:10)

您可以使用Reader monad获得与在任何地方传递参数相同的效果。与普通的功能代码相比,应用风格可以使开销相当低,但它仍然很尴尬。这是配置问题最常见的解决方案,但我发现它并不令人满意;实际上,明确地传递参数通常不那么难看。

另一种选择是reflection包,它允许您通过类型类上下文传递这样的常见配置数据,这意味着您的代码不得更改为检测附加值,仅检测类型。基本上,您为程序中的每个输入/结果类型添加一个新的类型参数,以便在某个配置的上下文中运行的所有内容都具有与其类型中的该配置相对应的类型。该类型会阻止您使用多个配置意外混合值,并允许您在运行时访问相关配置。

这避免了以适用的方式编写所有内容的开销,同时仍然是安全的,并允许您混合多个配置。它比声音简单得多;这是an example

(完全披露:我参与了反思包。)

答案 2 :(得分:4)

我们新的HFlags库正是为了这个。

如果您希望看到类似示例的示例用法,请查看以下内容:

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

模块之间不需要任何参数传递,您可以使用简单的语法定义新标志。它在内部使用unsafePerformIO,但我们认为它以安全的方式执行,并且您不必担心自己。

有关于此内容的博文:http://blog.risko.hu/2012/04/ann-hflags-0.html

答案 3 :(得分:2)

另一个选项是GHC implicit parameters。这些给你的选项提供了一个不太痛苦的版本(2):中间类型签名被感染,但你不必更改任何中间代码。

以下是一个例子:

{-# 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

如果您使用参数True运行此程序,则会打印We are going to do_stuff_A;使用参数False打印We are going to do_stuff_B

我认为这种方法与the reflection package mentioned in another answer类似,我认为HFlags mentioned in the accepted answer可能是更好的选择,但我为完整性添加了这个答案。