Haskell:使用 unsafePerformIO 进行全局常量绑定(bind)

标签 haskell

有很多关于对全局可变变量谨慎使用 unsafePerformIO 的讨论,以及一些支持它的语言添加(例如 Data.Global )。我有一个相关但不同的问题:将其用于全局常量 绑定(bind)。这是我认为完全可以的用法:命令行解析。

module Main where

--------------------------------------------------------------------------------
import Data.Bool (bool)
import Data.Monoid ((<>))
import Options.Applicative (short, help, execParser, info, helper, fullDesc,
                            progDesc, long, switch)
import System.IO.Unsafe (unsafePerformIO)

--------------------------------------------------------------------------------
data CommandLine = CommandLine
  Bool               --quiet
  Bool               --verbose
  Bool               --force

commandLineParser = CommandLine

  <$> switch
    (  long "quiet"
    <> short 'q'
    <> help "Show only error messages.")

  <*> switch
    (  long "verbose"
    <> short 'v'
    <> help "Show lots of detail.")

  <*> switch
    (  long "force"
    <> short 'f'
    <> help "Do stuff anyway.")

{- Parse the command line, and bind related values globally for
convenience. This use of unsafePerformIO is OK since the action has no
side effects and it's idempotent. -}

CommandLine cQuiet cVerbose cForce 

  = unsafePerformIO . execParser $ info (helper <*> commandLineParser)
      ( fullDesc
     <> progDesc "example program"
      )

-- Print a message:
say     = say' $ not cQuiet -- unless --quiet
verbose = say' cVerbose     -- if --verbose
say'    = bool (const $ return ()) putStrLn

--------------------------------------------------------------------------------
main :: IO ()

main = do
  verbose "a verbose message"
  say "a regular message"

能够在全局引用 cQuietcVerbose 等而不是在需要时将它们作为参数传递是非常有值(value)的。毕竟,这正是全局标识符的用途:它们有一个单一的值,在程序的任何运行期间都不会改变——恰好该值是从外部世界初始化的,而不是在程序文本中声明的。

从原则上讲,对从外部获取的其他类型的常量数据做同样的事情是有意义的,例如来自配置文件的设置——但随后出现了一个额外的问题:获取这些设置的操作不是幂等的,这与读取命令行不同(我在这里稍微滥用了“幂等”一词,但相信我已经理解了)。这只是添加了操作必须只执行一次的约束。我的问题是:使用这种形式的代码最好的方法是什么:

data Config = Foo String | Bar (Maybe String) | Baz Int

readConfig :: IO Config
readConfig = do …

Config foo bar baz = unsafePerformIO readConfig

doc向我建议这就足够了,不需要那里提到的任何预防措施,但我不确定。我看到有人提议添加受 do-notation 启发的顶级语法,专门针对这种情况:

Config foo bar baz <- readConfig

……这似乎是个好主意;我宁愿确保该操作最多执行一次,也不愿依赖各种编译器设置,并希望不会出现破坏现有代码的编译器行为。

我觉得这些实际上是常量,尽管它们从未改变,但明确传递这些东西所涉及的丑陋,强烈主张有一种安全和受支持的方式来做到这一点。不过,如果有人认为我在这里遗漏了重要的一点,我愿意听到相反的意见。

更新

  • 示例中的 sayverbose 用法并不是最好的,因为它不是 IO monad 中的值真正的烦恼——这些可以很容易地从全局 IORef 中读取参数。问题是在纯代码中普遍使用此类参数,必须全部重写以显式获取参数(即使这些参数不会改变,因此不需要是函数参数),或转换为 IO 更糟。我会在有空的时候改进这个例子。

  • 另一种思考方式:我正在谈论的行为类别可以通过以下笨拙的方式获得:运行一个通过 I/O 获取一些数据的程序;将结果作为一些全局绑定(bind)的值代入到主程序的模板文本中;然后编译并运行生成的主程序。然后,您将安全地拥有在整个程序中轻松引用这些常量的优势。看来直接实现这个模式应该不难。我在问题中提到了 unsafePerformIO,但我真的很想了解这种行为,以及获得它的最佳方式是什么。 unsafePerformIO 是一种方法,但它有缺点。

已知限制:

  • 使用unsafePerformIO,数据获取操作发生的时间是不固定的。这可能是一个功能,例如当且仅当实际使用该参数时,才会发生与缺少配置参数相关的错误。如果您需要不同的行为,则必须根据需要使用 seq 强制值。

最佳答案

我不知道我是否认为顶级命令行解析总是可以的!具体来说,观察当用户提供错误输入时此备用 main 会发生什么。

main = do
  putStrLn "Arbitrary program initialization"
  verbose "a verbose message"
  say "a regular message"
  putStrLn "Clean shutdown"
> ./commands -x
Arbitrary program initialization
Invalid option `-x'

Usage: ...

现在在这种情况下,您可以强制一个(或全部!)纯值,以便已知解析器已在明确定义的时间点运行。

main = do
  () <- return $ cQuiet `seq` cVerbose `seq` cForce `seq` ()
  -- ...
> ./commands -x
Invalid option `-x'
...

但是如果你有类似的东西会发生什么——

forkIO (withArgs newArgs action)

唯一明智的做法是 {-# NOINLINE cQuiet #-} 和 friend ,所以 System.IO.Unsafe 中的一些预防措施确实适用于您.但这是一个需要修补的有趣案例,请注意,您已经放弃了使用替代值运行子计算的能力。例如使用 localReaderT 解决方案没有这个缺点。

在读取配置文件的情况下,这对我来说似乎是一个更大的缺点,因为长时间运行的应用程序通常是可重新配置的,不需要停止/启动循环。顶级纯值排除重新配置。

但如果您考虑配置文件和命令行参数的交集,这可能会更清楚。在许多实用程序中,命令行上的参数会覆盖配置文件中提供的值,鉴于您现在所拥有的,这是不可能的行为。

对于玩具,当然,要疯狂。对于其他任何事情,至少使您的顶级值成为 IORefMVar。不过,仍有一些方法可以使非 unsafePerformIO 解决方案更好。考虑——

data Config = Config { say     :: String -> IO ()
                     , verbose :: String -> IO ()
                     }

mkSay :: Bool -> String -> IO ()
mkSay quiet s | quiet     = return ()
              | otherwise = putStrLn s

-- In some action...
  let config = Config (mkSay quietFlag) (mkVerbose verboseFlag)

compute :: Config -> IO Value
compute config = do
  -- ...
  verbose config "Debugging info"
  -- ...

这也尊重 Haskell 函数签名的精神,因为现在很清楚(甚至不需要考虑 IO 的开放世界)你的函数的行为实际上取决于程序配置。

关于Haskell:使用 unsafePerformIO 进行全局常量绑定(bind),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38385666/

相关文章:

Haskell 左箭头运算符替代

haskell - 简单的unix域套接字服务器

haskell - 有没有双向分配之类的东西?我在这里需要什么功能?

haskell - Powerset-over-Reader monad 是否存在?

haskell - 也许是一堆变压器里面的单子(monad)

haskell - 在 Haskell 中导出任意函数

haskell - 如何加速(或内存)一系列相互递归的函数

haskell - 为什么 ParsecT 没有 MonadWriter 实例?

string - GHC 接受的 unicode 字符范围

haskell - 证明自由单子(monad)的仿函数定律;我做对了吗?