-1

I would like to write Hangman game https://github.com/fokot/reactive-hangman/blob/master/src/Hangman.hs with seeing list of user actions as lazy stream. My recursive versions works ok (in code runGameRecursively (newGameState "secret"))

I got stuck on laziness issue

updateGameState :: GameState -> IO GameState
updateGameState gs = do
   l <- getALetter gs
   return $ updateState gs l

ff :: (a -> Bool) -> [IO a] -> IO a
ff f (i:is) = do
  res <- i
  if f res then return res else ff f is

runGameInfinite :: GameState -> IO ()
runGameInfinite gs =
  -- infinite lazy game loop
  let repl = tail $ iterate (\x -> x >>= updateGameState) (return gs) :: [IO GameState]
  in do
    endState <- ff gameEnded repl
    putStrLn $ showState endState

main = runGameInfinite (newGameState "car")

When you run the game every single step in repl need to reevaluate all previous even if they already were. I tried to play with $! but did not find correct answer ho to do it yet. Thanks

user1698641
  • 211
  • 1
  • 11
  • 2
    "I'm not able to do it" isn't a question - you've included a single line of code, but not even the error message you get when you run this hypothetical code. If you tried it and it gave an error, why not include the error to save people some time? You should isolate your problem to code which is small enough to put in your question – user2407038 Jan 20 '16 at 23:10
  • Ok I played with it and went further and edited the question. – user1698641 Jan 21 '16 at 23:34
  • 1
    Well the issue is evident now - IO is not lazy. Using IO like you have is certainly an antipattern. If you want this to work with IO, you'll have to use `unsafeInterleaveIO`, which is obviously terrible. Better that you remove the logic of your game from IO, then the desired semantics (i.e. `takeWhile p someInfiniteList` and variants) will work as expected. Instead of having `updateGameState :: GameState -> IO GameState` you must have `updateGameState :: UserInput -> GameState -> GameState` and then `readUserInput :: IO [UserInput]` is trivial - just `map read . lines getContents`. – user2407038 Jan 21 '16 at 23:43

1 Answers1

2

I think the scheme of using iterate to make an ostensibly pure list of IO actions is the source of the trouble here. Your plan is to update state by user input, but to consider the succession of states as a stream that you can 'treat like a list'. If I use a genuine iterateM to produce a proper stream things, then things go exactly as you were wanting them to go. So if I add the imports

import Streaming -- cabal install streaming
import qualified Streaming.Prelude as S

and after your main definitions write something like

runGameInfiniteStream gs =  S.print $ S.take 1 $ S.dropWhile (not . gameEnded) steps
  where
  steps :: Stream (Of GameState) IO ()
  steps = S.iterateM updateGameState (return gs)

main :: IO ()
main = runGameInfiniteStream (newGameState "car")

then I get

>>> main
You have 5 lifes. The word is "___"
Guess a letter: 
c
You have 5 lifes. The word is "c__"
Guess a letter: 
a
You have 5 lifes. The word is "ca_"
Guess a letter: 
r
GameState {secretWord = "car", lives = 5, guesses = "rac"}

I think this is exactly the program you intended, but using a proper stream concept rather than mixing IO and lists in some complicated way. Something similar could be done with pipes and conduit and similar packages.


(Added later:)

To stream to states corresponding to a pure list of Chars (emulating the result coming from user input), you can just use scan

pureSteps
   :: (Monad m) => GameState -> [Char] -> Stream (Of GameState) m ()
pureSteps gs chars = S.scan updateState gs id (S.each chars)

this is basically the same as Prelude.scanl which can also be used (in the pure case) to view the updates:

>>> S.print $ pureSteps (newGameState "hi") "hxi"
GameState {secretWord = "hi", lives = 5, guesses = ""}
GameState {secretWord = "hi", lives = 5, guesses = "h"}
GameState {secretWord = "hi", lives = 4, guesses = "h"}
GameState {secretWord = "hi", lives = 4, guesses = "ih"}

>>> mapM_ print $ scanl updateState (newGameState "hi") "hxi"
GameState {secretWord = "hi", lives = 5, guesses = ""}
GameState {secretWord = "hi", lives = 5, guesses = "h"}
GameState {secretWord = "hi", lives = 4, guesses = "h"}
GameState {secretWord = "hi", lives = 4, guesses = "ih"}

To view the final 'winning' state, if it exists, you can write, e.g.

runPureInfinite
  :: Monad m => GameState -> [Char] -> m (Of [GameState] ())
runPureInfinite gs = S.toList . S.take 1 . S.dropWhile (not . gameEnded) . pureSteps gs

-- >>> S.print $ runPureInfinite (newGameState "car") "caxyzr"
-- [GameState {secretWord = "car", lives = 2, guesses = "rac"}] :> ()

and so on.

Michael
  • 2,831
  • 15
  • 16
  • Thanks for help Michael, this is exactly what I wanted. I have one related question. How can I construct function for testing where I will just pass [Char] instead of IO. I know I can use S.each and return Stream but I want to be runGameInfiniteStream as big as possible. – user1698641 Jan 26 '16 at 03:49
  • I took out the update state function ```runGameInfiniteStream :: Monad m => (GameState -> m GameState) -> GameState -> Stream (S.Of GameState) m () runGameInfiniteStream updateGameStateFunction gs = S.take 1 $ S.dropWhile gameInProgress steps where --steps :: Monad m => GameState -> (GameState -> m GameState) -> Stream (S.Of GameState) m () steps = S.iterateM updateGameStateFunction (return gs)``` How can I construct one which will allow me to pass inside [Char] and will update the state accordingly. I tried to use StateMonad for that but without success. – user1698641 Jan 26 '16 at 03:52
  • Something l ike `pureSteps gs chars = S.scan updateState gs id (S.each chars)` yields the updated states corresponding to a list of character inputs. (`scan` is complicated by an extra field to fit with the `foldl` library) – Michael Jan 26 '16 at 19:19
  • Then something like `runPure txt inputs = S.toList $ S.take 1 $ S.dropWhile (not . gameEnded) $ pureSteps (newGameState txt) inputs` will give a 0- or 1-member list with the end result, given a list of Chars as 'inputs'. So if I write `runPure "car" "cxar"` in ghci, I get `[GameState {secretWord = "car", lives = 4, guesses = "rac"}] :> ()` – Michael Jan 26 '16 at 19:21