我有一些函数
app :: StateT AppState IO ()
,它可以在进行大量计算和IO的同时维护一些应用程序状态(我已经定义了main = void $ runStateT app initialState
)。我想注册一个信号处理程序handler :: StateT AppState IO ()
,它可以通过编写类似的内容来查看和修改当前状态
installHandler sigINT (Catch handler) Nothing
这不起作用,因为
Catch :: IO () -> Handler
和 handler :: StateT AppState IO ()
。
我尝试过以下方法:
installHandler . Catch . pure <$> handler <*> pure Nothing
这行代码被评估后立即运行
handler
(因为我们使用的是 StateT
应用程序)。此外,Catch . pure <$> handler
与Catch . pure $ ()
相同,因为我们运行副作用来检索()
,然后应用pure
只是将其包装在IO
中,而不捕获这些副作用。
有没有一种方法可以注册信号处理程序,以便它在不破坏
StateT
抽象的情况下实际执行操作?我将不胜感激任何建议!
另外,如果我使用“副作用”这个短语激怒了任何人,我很抱歉。
根据 @chi 的评论,没有办法让这个精确的设计发挥作用。
信号是通过单独的 IO 线程处理的,因此实现这一点的最直接方法是通过应用程序 monad“已知”的共享可变引用,并通过处理函数的闭包提供。
如果处理程序只需要访问状态的一小部分,并且该小部分在应用程序的其余部分中没有太多使用,那么最简单的方法是向应用程序状态添加一个字段来存储
IORef
或 MVar
到那一小块状态。
特别是,如果您只是尝试在处理程序中设置一个将在应用程序循环中“注意到”的标志,那么
IORef
就可以了。以下程序说明了一般方法:
{-# LANGUAGE OverloadedRecordDot #-}
import System.Posix.Signals
import Control.Concurrent
import Control.Monad.State
import Data.IORef
data AppState = AppState
{ count :: Int
, flagINT :: IORef Bool
}
app :: StateT AppState IO ()
app = do
x <- gets count
modify $ \s -> s{count=s.count+1}
liftIO $ putStrLn $ "Running " ++ show x
liftIO $ threadDelay 1000000
quit <- gets flagINT >>= liftIO . readIORef
if quit then do
liftIO $ putStrLn "Quitting"
else app
main = do
flagINT_ <- newIORef False
installHandler sigINT (Catch (writeIORef flagINT_ True)) Nothing
runStateT app (AppState 0 flagINT_)
在这里,我已在
main
中安装了处理程序,我可以直接访问在那里创建的 flagInt_
可变引用。或者,您可以通过从状态读取标志引用来在应用程序 monad 中设置处理程序:
setINTHandler :: StateT AppState IO ()
setINTHandler = do
flagINT_ <- gets flagINT
liftIO $ installHandler sigINT (Catch (writeIORef flagINT_ True)) Nothing
pure ()
有几点需要注意:
flagInt
是可变 State
的字段,但这只是为了方便。 flagInt
仅作为只读字段访问,为实际可变标志提供可变引用。它也可以是单独的 Reader
的一部分。IORef
和非原子 writeIORef
对于这个特定示例来说是足够线程安全的。如果您在处理程序中对状态执行更复杂的操作),那么您可能需要更加小心,也许改用 MVar
。