Haskell:使用unsafePerformIO进行全局常量绑定
对于全局可变变量使用unsafePerformIO
以及支持它的一些语言补充(例如Data.Global
),有许多讨论。 我有一个相关但不同的问题:将其用于全局常量绑定。 以下是我认为完全正确的用法:命令行解析。
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
等非常有价值,而不必在任何需要的地方将它们作为参数传递。 毕竟,这正是全局标识符的用途:这些值在程序的任何运行过程中都有一个永不变化的值 - 它只是发生在外部世界而非程序文本中声明的值。
原则上,对于从外部获取的其他类型的常量数据(例如,来自配置文件的设置)执行相同的操作是有意义的,但是会产生额外的一点:与读取命令行不同,获取这些数据的操作不是幂等的我在这里略微滥用“幂等”这个词,但相信我是理解的)。 这只是增加了行为必须只执行一次的限制。 我的问题是:用这种形式的代码做什么最好的方法是:
data Config = Foo String | Bar (Maybe String) | Baz Int
readConfig :: IO Config
readConfig = do …
Config foo bar baz = unsafePerformIO readConfig
该文件告诉我,这是足够的,没有需要提到的预防措施,但我不确定。 我已经看到了针对这种情况增加受注释启发的顶级语法的建议:
Config foo bar baz <- readConfig
......这似乎是一个非常好的主意; 我宁愿确保该操作最多只会执行一次,而不是依赖各种编译器设置,并希望编译器不会出现违反现有代码的行为。
我认为这些事实上都是常数,并且尽管事实上他们从来没有改变过,但这些丑陋事件明确地传递了这样的事情,但强烈地争辩说有一种安全和受支持的方式来做到这一点。 不过,如果有人认为我错过了重要的一点,我很乐意听取相反的意见。
更新
示例中的say
和verbose
用法并不是最好的,因为IO
monad中的值不是真正的烦恼 - 它们可以轻松地从全局IORef
读取参数。 问题是在纯代码中普遍使用这些参数,这些参数必须全部重写,以便明确地接受参数(即使这些参数没有改变,因此不需要是函数参数),也可以转换为IO
更糟。 当我有时间时,我会改进这个例子。
另一种思考方式:我谈论的行为类可以通过以下笨重的方式获得:运行通过I / O获取一些数据的程序; 取得结果并将其替换为主程序的模板文本,作为某些全局绑定的值; 然后编译并运行生成的主程序。 然后,您将安全地在整个程序中轻松地引用这些常量。 似乎直接实施这种模式应该不那么困难。 我说了一个提到unsafePerformIO
的问题,但是我真的有兴趣了解这种行为,以及获得它的最好方法是什么。 unsafePerformIO
是一种方式,但它有缺点。
已知的限制:
unsafePerformIO
时,发生数据获取操作时不固定。 这可能是一个功能,因此,如果且仅当实际使用该参数时才会发生与缺少配置参数相关的错误。 如果你需要不同的行为,你必须根据需要用seq
强制这些值。 我不知道我是否会认为顶级命令行解析总是OK! 具体来说,当用户提供错误的输入时,观察这个替代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 #-}
和朋友,因此System.IO.Unsafe
一些预防措施确实适用于您。 但是这是一个有趣的案例,请注意,您已经放弃了使用替代值运行子计算的能力。 例如使用local
ReaderT
解决方案没有这个缺点。
在阅读配置文件的情况下,这似乎是一个更大的缺点,因为长时间运行的应用程序通常可以重新配置,而不需要停止/启动周期。 顶级纯价值排除重新配置。
但是,如果考虑到你的配置文件和你的命令行参数的交集,这可能会更加清晰。 在命令行中的许多实用程序参数中都会覆盖配置文件中提供的值,这是给予您现在所拥有的不可能的行为。
对于玩具,当然,要疯狂地生猪。 对于其他任何事情,至少让你的顶级价值是IORef
或MVar
。 unsafePerformIO
,仍然有一些方法可以使非安全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的开放世界),您的函数的行为实际上取决于程序配置。
想起Hackage的两个案例:
包cmdargs使用unsafePerformIO
- 将命令行参数视为常量。
在包oeis中 ,“纯”函数getSequenceByID
使用unsafePerformIO从http://oeis.org上的网页返回内容。 它在文件中指出:
请注意,结果不在IO monad中,即使实现需要通过Internet查找信息。 没有副作用可言,并且从实际的角度来看,该功能是相对透明的(OEIS A-数字可能会在理论上发生变化,但极不可能)。
-XImplicitParams在这种情况下很有用。
{-# LANGUAGE ImplicitParams #-}
data CommandLine = CommandLine
Bool --quiet
Bool --verbose
Bool --force
say' :: Bool -> String -> IO ()
say' = bool (const $ return ()) putStrLn
say, verbose :: (?cmdLine :: CommandLine) => String -> IO ()
say = case ?cmdLine of CommandLine cQuiet _ _ -> say' $ not cQuiet
verbose = case ?cmdLine of CommandLine _ cVerbose _ -> say' cVerbose
任何隐含类型并使用say
或verbose
的?cmdLine :: CommandLine
都会将?cmdLine :: CommandLine
隐式参数添加到其类型中。
:type (s -> say (show s))
(s -> say (show s))
:: (Show a, ?cmdLine::CommandLine) => a -> IO ()
链接地址: http://www.djcxy.com/p/43251.html
上一篇: Haskell: use of unsafePerformIO for global constant bindings