I guess my question could be distilled to a Y/N question: "Can the IO monad keep state only via I/O actions?" In other words, is my understanding correct that if I have to write an action
run :: IO String
that is executed repeatedly by some other IO monad instance, there's no way I can write it so that it keeps memory, from a call to the next, of a state, other than by serializing the state somewhere (e.g. to file or in the caller if it offers an API to do so)?
But if the answer is "Yes, you can't do that", then question is the one in the title: how can I write a monitor for XMobar which keeps a local state?
I'm experimenting with xmobar, and in particular with its plugins and monitors.
All monitors are run via Run $ SomeMonitor where SomeMonitor must be of type Runnable, i.e. it can be any type implementing the (Exec r, Read r, Show r) interface, of which Exec is the interesting one, because it's where you put the business logic of the plugin, as it has type IO String, so it can create the String that is displayed by XMobar using IO.
Here was my first, very easy, experiment,
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
where I determine how many updates are possible via pacman on my ArchLinux system.
Now, clearly the value shown by XMobar changes over time, but not because it has an explicit dependency on time; it's because the state of the system changes, and the change is retrieved via an I/O action, expressed in the code above by getCommandOutput "checkupdates".
But what if I wanted a plugin with an explicit dependency on time? I mean, a plugin that updates every second and shows, in turn, one of several strings? Say it shows first "A long time ago", then "in a galaxy far,", and finally "far away...", and then loops again.
My requirement makes me think of State, but the Exec constraint is forcing me in the IO monad, so I don't think that the StateT transformer is the way to go, because I can't use another monad wrapping IO in run. I'd rather need IO to wrap some state... but I can't change IO!
Therefore the only way I see to keep state is for serializing it somewhere, and then re-reading it:
instance Exec MyPlugin where
rate _ = 1000
run _ = do
old <- getCurrentStateOfSelf
return $ makeNewState old
where makeNewState is String -> String¹, but getCurrentStateOfSelf should be provided by XMobar's API, otherwise what'd be left? Writing the state to a file?
run _ = do
old <- readStateFromFile
let new = makeNewState old
writeStateToFile new
return new
This would be horrible.
I tried asking ChatGPT, and it gave me this:
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
but I don't think that's a solution, because main is still in charge of making multiple calls to runStateT piping the state from one call to another; it's not like multiple calls to main are accessing successive states of func, which is what I'd want.
I guess probably the answer is that I'm hitting a wall: XMobar montitors have been designed in the IO monad whereas I'd need the State monad (then clearly XMobar would still use the IO monad to show stuff on screen, because after all it is a program, so it comes from compiling a main function, which is IO ()).
(¹) In the case of the 3 distinct strings above, it could be a closure with a [String] local state that takes a String, locates it in the cycle state, and gives back the item after that.
It sorta depends. What is the API by which you give these IO actions to xmobar? If you can perform IO of your own to create an IO action, you can construct an IORef that lives outside of the IO action itself, for it to refer to and store data in. But if you can't interact with how xmobar starts, and all it asks you for is some IO a actions, you can't really smuggle an outside IORef in there (unless you cheat with unsafePerformIO).
But let's imagine that you are running xmobar yourself, and it returns some kind of IO action:
runXmobar :: [IO String] -> IO ()
runXmobar = undefined -- implemented by xmobar
Your main can look something like this:
main = do
ref <- newIORef 0
runXmobar [uptick ref]
uptick :: IORef Int -> IO String
uptick r = atomicModifyIORef' r next
where next x = (x + 1, show x)
Here we've achieved what you asked for in your TL;DR question: an IO String action (uptick ref) that saves state locally to produce a different result each time it's called.
How to fit that into an Exec instance? It's not hard. duplode outlines it in a comment on this answer: store the IORef as a field in your object so that the run action can refer to it.
data MyPlugin = MyPlugin (IORef Int)
instance Exec MyPlugin where
-- Read, Show, etc...
run (MyPlugin r) = atomicModifyIORef' r next
where next x = (x + 1, show x)
Construct this MyPlugin object in the main in your xmobar.hs, and include it in your config. You won't be able to define config as a top-level value anymore, because it depends on a value constructed in main by IO, but you can either define it in main, or make it a function taking a MyPlugin object as an argument.
main = do
myplugin <- MyPlugin <$> newIORef 0
let config = defaultConfig {
-- ...
commands = [
-- ...
, Run myplugin
]
}
xmobar =<< configFromArgs config
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With