如何为XMobar编写可跨调用保留状态的监视器?
摘要
我的问题可以简化为一个是非题:“IO monad是否只能通过I/O操作保留状态?” 换句话说,我的理解是否正确:如果我需要编写一个被其他IO monad实例反复执行的操作
run :: IO String
那么除了将状态序列化到某处(例如文件或调用者提供的API)之外,无法让它在多次调用间保留状态?
如果答案是*“是的,无法直接实现”*,那么我的核心问题就是:如何为XMobar编写一个能保留本地状态的监视器?
详细背景
我正在试验xmobar,尤其是它的插件与监视器功能。
所有监视器都通过Run $ SomeMonitor运行,其中SomeMonitor必须是Runnable类型,即实现了(Exec r, Read r, Show r)接口的类型。其中Exec是关键接口,因为插件的业务逻辑需要写在这里,它的类型是IO String,可以通过IO操作生成XMobar显示的字符串。
我最初做了一个简单的实验:
data ArchUpdates = ArchUpdates deriving (Read, Show) instance Exec ArchUpdates where rate _ = 36000 run _ = fmap (makeMessage . length . lines) $ getCommandOutput "checkupdates" where makeMessage :: Int -> String makeMessage = show -- simplified version
这个插件用于查看我的ArchLinux系统中通过pacman可更新的包数量。
显然,XMobar显示的值会随时间变化,但这并非因为它显式依赖时间,而是因为系统状态发生了变化,这种变化通过I/O操作getCommandOutput "checkupdates"获取。
但如果我想开发一个显式依赖时间的插件呢?比如每秒更新一次,依次显示不同的字符串:先显示"A long time ago",然后是"in a galaxy far,",最后是"far away...",循环往复。
我的需求让我想到了State monad,但Exec约束强制我使用IO monad,所以我认为StateT转换器不是可行方案,因为我无法在run中使用其他包裹IO的monad。我需要IO包裹状态,但又无法修改IO!
因此,我能想到的唯一保留状态的方法是将其序列化到某处,然后重新读取:
instance Exec MyPlugin where rate _ = 1000 run _ = do old <- getCurrentStateOfSelf return $ makeNewState old
其中makeNewState的类型是String -> String¹,但getCurrentStateOfSelf需要XMobar的API提供,否则就只能将状态写入文件?
run _ = do old <- readStateFromFile let new = makeNewState old writeStateToFile new return new
这种方法太糟糕了。
我尝试询问ChatGPT,它给出了以下代码:
import Control.Monad.State func :: StateT Int IO String func = do currentState <- get liftIO $ putStrLn $ "Current state: " ++ show currentState modify (+1) return "Hello, World!" main :: IO () main = do (result, newState) <- runStateT func 0 putStrLn $ "Result: " ++ result putStrLn $ "Final state: " ++ show newState
但我认为这不是解决方案,因为main需要负责多次调用runStateT并传递状态,而不是让func在多次调用间自动保留状态,这才是我想要的效果。
我猜测自己遇到了瓶颈:XMobar监视器基于IO monad设计,而我需要的是State monad(当然XMobar仍会使用IO monad在屏幕上显示内容,因为它本质是一个程序,编译自main函数,类型为IO ())。
(¹) 对于上述三个字符串的场景,它可以是一个带有[String]本地state的闭包,接收一个String,在cycle state中找到它,然后返回下一个元素。
内容的提问来源于stack exchange,提问作者Enlico

