有很多关于对全局可变变量谨慎使用 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"
能够在全局引用 cQuiet
、cVerbose
等而不是在需要时将它们作为参数传递是非常有值(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
……这似乎是个好主意;我宁愿确保该操作最多执行一次,也不愿依赖各种编译器设置,并希望不会出现破坏现有代码的编译器行为。
我觉得这些实际上是常量,尽管它们从未改变,但明确传递这些东西所涉及的丑陋,强烈主张有一种安全和受支持的方式来做到这一点。不过,如果有人认为我在这里遗漏了重要的一点,我愿意听到相反的意见。
更新
示例中的
say
和verbose
用法并不是最好的,因为它不是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
中的一些预防措施确实适用于您.但这是一个需要修补的有趣案例,请注意,您已经放弃了使用替代值运行子计算的能力。例如使用 local
的 ReaderT
解决方案没有这个缺点。
在读取配置文件的情况下,这对我来说似乎是一个更大的缺点,因为长时间运行的应用程序通常是可重新配置的,不需要停止/启动循环。顶级纯值排除重新配置。
但如果您考虑配置文件和命令行参数的交集,这可能会更清楚。在许多实用程序中,命令行上的参数会覆盖配置文件中提供的值,鉴于您现在所拥有的,这是不可能的行为。
对于玩具,当然,要疯狂。对于其他任何事情,至少使您的顶级值成为 IORef
或 MVar
。不过,仍有一些方法可以使非 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/