Reputation: 5949
I have done some research on stackoverflow to find viable solution to the common problem of maintaining different states of global variable.
I found this elaborate question that addresses similar concern. It raises important issue of godlike global variable and that's an antipattern in Haskell. I perfectly understand that my situation is similar and I am trying to introduce this antipattern, but I don't really like the answer. It seems that Netwire
is an overkill for my task at hand, it could be done in much more simple and elegant way.
And I also found this one, but both question and answers address more general concerns and approaches, while I have concrete problem and, hopefully, concrete solution. What I also want (and could not find in previous questions) is to make a qualitative step in understanding of maintaining variable states through the simple example.
In the code below I am trying to update state of the godlike variable from two different places executing :load
and :new
commands, but, obviously, it doesn't work.
My question is how to modify following code in order to accommodate possibility of changing global variable value in a functional way? Should I throw away all the code because it represents imperative-style approach and replace it totally new parseInput
that follows rules of functional world? Should I replace global variable with something else? I assume I could use IORef
somehow, it seems appropriate. Or ST Monad
as this question/answer recommends.
What would be the easiest and straightforward step to address this problem without an overkill? I understand that I might need to better grasp the notion of Monads (State Monad in particular) and I am ready to learn how they could help with addressing this particular problem. But the articles I have read so far (this and this), didn't help much. I assume that State Monad is not really appropriate because my example does not have return value, only updated state. If I am wrong, could you please explain how and what missing links would help me to understand states in Haskell better?
{-# LANGUAGE QuasiQuotes #-}
import Text.Regex.PCRE
import System.Console.Haskeline
import TH (litFile)
import System.FilePath
import System.IO
import Control.Monad
import Control.Monad.IO.Class
import Data.List
mydata :: [Int]
mydata = [0]
saveDataToFile :: [Int] -> IO ()
saveDataToFile mydata = withFile "data.txt" WriteMode $ \h -> System.IO.hPutStr h (unwords $ map show mydata)
loadDataFromFile :: [Int]
loadDataFromFile = map read . words $ [litFile|data.txt|]
help :: InputT IO ()
help = liftIO $ mapM_ putStrLn
[ ""
, ":help - this help"
, ":q - quit"
, ":commands - list available commands"
, ""
]
commands :: InputT IO ()
commands = liftIO $ mapM_ putStrLn
[ ""
, ":show - display data"
, ":save - save results to file"
, ":load - loads data from file"
, ":new - generate new element "
, ""
]
parseInput :: String -> InputT IO ()
parseInput inp
| inp =~ "^\\:q" = return ()
| inp =~ "^\\:he" = help >> mainLoop
| inp =~ "^\\:commands" = commands >> mainLoop
| inp =~ "^\\:show" = do
liftIO $ putStrLn $ unwords $ map show mydata
mainLoop
| inp =~ "^\\:save" = do
liftIO $ saveDataToFile mydata
mainLoop
| inp =~ "^\\:load" = do
let mydata = loadDataFromFile -- <-- should update mydata
mainLoop
| inp =~ "^\\:new" = do
let mydata = mydata ++ [last mydata + 1] -- <-- should update mydata
mainLoop
| inp =~ ":" = do
outputStrLn $ "\nNo command \"" ++ inp ++ "\"\n"
mainLoop
| otherwise = handleInput inp
handleInput :: String -> InputT IO ()
handleInput inp = mainLoop
mainLoop :: InputT IO ()
mainLoop = do
inp <- getInputLine "% "
maybe (return ()) (parseInput) inp
greet :: IO ()
greet = mapM_ putStrLn
[ ""
, " MyProgram"
, "=============================="
, "For help type \":help\""
, ""
]
main :: IO ()
main = do
greet
runInputT defaultSettings (mainLoop)
PS. I use Template Haskell definitions (TH module) from this answer.
Upvotes: 2
Views: 596
Reputation: 24156
One clean way to handle this is to add StateT
to your transformer stack.
Instead of using the type InputT IO
you'd use either StateT [Int] (InputT IO)
or InputT (StateT [Int] IO)
. Since InputT
has more operations to mess around with lifting, I'd use InputT (StateT [Int] IO)
to keep the complicated operations on the outside.
To make things simple I'd add an orphan MonadState
instance for MonadState m => MonadState (InputT m)
instance MonadState s m => MonadState s (InputT m) where
get = lift get
put = lift . put
state = lift . state
Then when you want to modify the state you'd use get
, put
, or state
.
| inp =~ "^\\:new" = do
mydata <- get -- reads the state
put $ mydata ++ [last mydata + 1] -- updates the state
mainLoop
You can then clean up the type signatures to make your code more general. Instead of only working on InputT (StateT [Int] IO)
you can make the code work for (MonadState [Int] m, MonadIO m) => InputT m
.
To run a StateT
use runStateT
. If you change the type of mainloop
to InputT (StateT [Int] IO) ()
or the more general (MonadState [Int] m, MonadIO m) => InputT m ()
then you can run it with
main :: IO ()
main = do
greet
runStateT (runInputT defaultSettings mainLoop) []
-- ^ ^ run the outer InputT ^
-- run the inner StateT ..... with starting state []
Upvotes: 5