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"

能够全局引用cQuietcVerbose等非常有价值,而不必在任何需要的地方将它们作为参数传递。 毕竟,这正是全局标识符的用途:这些值在程序的任何运行过程中都有一个永不变化的值 - 它只是发生在外部世界而非程序文本中声明的值。

原则上,对于从外部获取的其他类型的常量数据(例如,来自配置文件的设置)执行相同的操作是有意义的,但是会产生额外的一点:与读取命令行不同,获取这些数据的操作不是幂等的我在这里略微滥用“幂等”这个词,但相信我是理解的)。 这只是增加了行为必须只执行一次的限制。 我的问题是:用这种形式的代码做什么最好的方法是:

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

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

我认为这些事实上都是常数,并且尽管事实上他们从来没有改变过,但这些丑陋事件明确地传递了这样的事情,但强烈地争辩说有一种安全和受支持的方式来做到这一点。 不过,如果有人认为我错过了重要的一点,我很乐意听取相反的意见。

更新

  • 示例中的sayverbose用法并不是最好的,因为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解决方案没有这个缺点。

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

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

    对于玩具,当然,要疯狂地生猪。 对于其他任何事情,至少让你的顶级价值是IORefMVarunsafePerformIO ,仍然有一些方法可以使非安全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
    

    任何隐含类型并使用sayverbose?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

    下一篇: How do the operators `>>>` and `>>=` work in Haskell?