altern
altern

Reputation: 5949

Haskell - maintaining different states of global variable

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

Answers (1)

Cirdec
Cirdec

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

Related Questions