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.
The general approach in amalloy's answer of initialising an IORef
in the setup code and then making it available to the loop action is sound. However, as we have found out, by doing the initialisation in the main
of xmobar.hs
and trying to pass it through the plugin type we end up swimming against the current of the arrangement xmobar expects for its plugins. We can instead adapt the idea while implementing Exec
in terms of start
(:: Exec e => e -> (String -> IO ()) -> IO ()
) rather than run
(:: Exec e => e -> IO String
), at the modest cost of having to lay down the updating loop ourselves. (The snippet below is partly based on the Date
example from xmobar's docs.)
import Xmobar
import Data.IORef
import Control.Monad (forever)
data MyPlugin = MyPlugin
deriving (Read, Show)
instance Exec MyPlugin where
start MyPlugin callback = do
ref <- newIORef 0
forever $ do
output <- atomicModifyIORef' ref next
callback output
tenthSeconds 10 -- Wait one second.
where
next x = (x + 1, show x)
-- Then add MyPlugin to your config's commands and template as usual.
Incidentally, since start
allows us to write the update loop, using StateT
for the updates now becomes viable as well:
import Xmobar
import Control.Monad.State.Strict
import Control.Monad (forever)
-- etc.
instance Exec MyPlugin where
start MyPlugin callback = evalStateT loop 0
where
next = gets show <* modify' (+ 1)
loop = forever $ do
output <- next
liftIO $ do
callback output
tenthSeconds 10