正确的方式对待在Haskell全局标志(Proper way to treat global fla

2019-07-29 03:09发布

我经常需要做的是在很多地方用某种方式配置的核心功能 - 也就是说,它可以使用两种算法A或根据命令行开关算法B; 或者它打印额外详细信息,如果一个“调试”标志设置莫名其妙地到stdout。

我应该如何实现这样的全球性的标志吗?

我看到4个选项,所有的人都没有真正的好。

  1. 从功能读取命令行参数 - 糟糕,因为需要IO单子和核心计算功能都是纯净的,我不想在那里得到IO;

  2. 从主/ IO传递参数,一路过关斩将到需要改变行为的“叶”功能 - 完全无法使用,因为这意味着改变在不同的模块无关的功能,一打就通过这个参数,我想尝试这样的配置选项多次不用每次都改变包装的代码;

  3. 使用unsafePerformIO得到一个真正的全局变量-感觉丑陋和矫枉过正这样一个简单的问题;

  4. 就在函数中有两个选项的代码和评论他们中的一个了。 或有功能do_stuff_A和do_stuff_B,并改变它们中的哪一个是根据什么是全局函数叫做needDebugInfo=True说。 这就是我现在正在做的事情为debuginfo ,但它不能改变W / O重新编译,它不应该真的是最好的方式...

我不需要或不想要全局可变状态 - 我想有一个简单的全局标志,这是在运行时不变,但可以在程序启动时被莫名其妙定置。 是否有任何选项?

Answer 1:

我们的新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



Answer 2:

这些天来,我更喜欢使用一个Reader单子构建应用程序的只读状态。 环境在启动时initalized,然后提供整个方案的顶层。

一个例子是xmonad :

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

在程序运行中的顶层部分X而不是IO ; 其中XConf是通过命令行标志(和环境变量)initalized的数据结构。

所述XConf状态然后可以作为纯数据对需要它的功能被传递。 随着NEWTYPE获得你也可以得到重用所有MonadReader代码,用于访问状态。

这种方法保留了2语义纯度,但给你少写代码,因为单子做管道。

我认为它的“真实”哈斯克尔的方式做只读的配置状态。

-

使用该方法unsafePerformIO初始化的全局状态也行,当然,但(当你使你的程序的并发或并行EG)往往最终咬你。 他们也有有趣的初始化语义 。



Answer 3:

您可以使用Reader单子来获得相同的效果传递参数到处。 应用型风格可以让比较正常的功能代码的开销相当低,但它仍然可以是相当尴尬的。 这是对配置问题最常见的解决方案,但我并不觉得非常满意; 事实上,围绕传递参数明确往往是不太难看。

另一种是反射包,它可以让你通过类型类上下文通过周围像这样围绕共同的配置数据,这意味着没有你的代码必须改变下探的附加价值,只有类型。 基本上,你一个新的类型参数添加到程序中的每个输入/结果类型,让一切有一定配置的范围内操作具有对应于在类型配置类型。 使用多个配置该类型停止不小心混合值,并给出了在运行时访问该相关联的配置。

这避免了在编写应用性款式应有尽有,同时仍然安全,使您可以混合多种配置的开销。 这是一个简单了很多比它的声音; 这里有一个例子 。

(全discloure:我上的反射封装工作。)



Answer 4:

另一种选择是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可能是一个更好的选择,但我加入这个答案的完整性。



文章来源: Proper way to treat global flags in Haskell