14

我经常需要使在许多地方使用的核心功能以某种方式可配置 - 即,它可以使用算法 A 或算法 B,具体取决于命令行开关;或者如果以某种方式设置了“调试”标志,则让它将额外的详细信息打印到标准输出。

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

我看到了 4 个选项,它们都不是很好。

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

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

  3. 用于unsafePerformIO获取真正的全局变量 - 对于这样一个简单的问题,感觉很丑陋和矫枉过正;

  4. 在函数的中间有两个选项的代码并将其中一个注释掉。或者具有函数 do_stuff_A 和 do_stuff_B,并根据全局函数的内容更改调用其中的哪一个needDebugInfo=True。这就是我现在正在为debuginfo.

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

4

4 回答 4

16

这些天来,我更喜欢使用monadReader构建应用程序的只读状态。该环境在启动时被初始化,然后在整个程序的顶层可用。

一个例子是 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也可以工作,但最终会咬你一口(例如,当你让你的程序并发或并行时)。它们还有有趣的初始化语义

于 2012-04-23T17:50:10.450 回答
10

您可以使用Readermonad 获得与到处传递参数相同的效果。与普通函数式代码相比,Applicative 样式可以使开销相当低,但仍然很尴尬。这是配置问题最常见的解决方案,但我觉得它不是非常令人满意;实际上,明确地传递参数通常不那么难看。

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

这避免了以应用风格编写所有内容的开销,同时仍然是安全的,并允许您混合多种配置。它比听起来简单得多。这是一个例子

(完全披露:我研究过反射包。)

于 2012-04-23T17:50:13.653 回答
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

于 2012-05-03T06:30:56.737 回答
2

另一种选择是GHC 隐式参数。这些为您的选项 (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.

我认为这种方法类似于另一个答案中提到的反射包,并且我认为接受的答案中提到的 HFlags可能是一个更好的选择,但为了完整起见,我添加了这个答案。

于 2013-10-05T01:03:45.543 回答