1

I'm trying to write a very simple game in Haskell. I have this loop which is the last part, and where I don't really know what to do. Of course it is a loop, so it has to run itself.

gameLoop :: User -> IO ()
gameLoop initUser = displayPosition initUser >> gameLoop (applyAction initUser)
  where
    applyAction :: UserAction
    applyAction = maybe id (unsafePerformIO (fmap getUserAction getLine))

getUserAction :: String -> Maybe UserAction is a function that looks up a string in a map and returns a UserAction :: User -> User. Then I do some ugly unpacking (unsafePerformIO), which I don't know to circumvent.

I thought this should run, because my type seems to be correct, but it still doesn't.

It says:

maybe is applied to too few arguments  AND
couldn't match a1 -> a0 -> a0 with actual type Maybe UserAction, because unsafePerformIO is applied to too few arguments.

I don't understand these errors. Can anybody explain how to solve these last problems, or how to get rid of unsafePerformIO?

2
  • I suggest you look at forever from Control.Monad. Commented May 4, 2017 at 2:48
  • Isn't that executing the same monad over and over? I want to pass along initUser. Commented May 4, 2017 at 2:56

2 Answers 2

3

Boilerplate Lecture

First off, pretend unsafePerformIO doesn't exist. Second, next time please present a more complete code snippet, I'll try to answer but will be making assumptions along the way as a result.

The Walk-through

You presented:

gameLoop :: User -> IO ()
gameLoop initUser = displayPosition initUser >> gameLoop (applyAction initUser)

So it seems you must have some definition for displayPosition :: User -> IO (). Then below you use UserAction which seems to be type UserAction = User -> User.

applyAction :: UserAction

Now you suddenly realize you don't want a User -> User type but instead have this IO you'd like to do, yielding a User -> IO User type:

applyAction = maybe id (unsafePerformIO (fmap getUserAction getLine))

Instead of making the IO magically, and totally unsafely, disappear you could could define:

applyAction :: User -> IO User
applyAction previousUser =
    do ln <- getLine
       case getUserAction ln of
           Nothing -> -- You never said what to do here.
                      -- This is the same logical issue as the missing
                      -- argument to your call to `maybe` above.
                      return previousUser -- XXX do something correct!
           Just act -> return act

Going back to the gameLoop, the types have changed and we can't use applyAction initUser :: IO User where the expected value is :: User. We can, however, use monadic bind or do-notation:

gameLoop initUser =
      do displayPosition initUser
         newUser <- applyAction initUser
         gameLoop newUser

This is just syntactic sugar for:

gameLoop initUser = displayPosition initUser >> applyAction initUser >>= \newUser -> gameLoop newUser

Or simply:

gameLoop initUser = displayPosition initUser >> applyAction initUser >>= gameLoop

More Rewrites

That was one solution, but it would be nice to keep the applyAction function effect-free (no IO) so you could test it and more easily refactor the program. Instead of getting a line there how about we get a line in the loop and pass it in:

gameLoop initUser =
      do displayPosition initUser
         command <- getLine
         newUser <- applyAction command initUser
         gameLoop newUser

applyAction :: String -> User -> User
applyAction cmd oldState = maybe oldState id (getUserAction ln)
Sign up to request clarification or add additional context in comments.

Comments

1

In order to not use unsafePerformIO, use IO. Try this:

getUserAction <$> getLine :: IO (Maybe UserAction)

<$> is fmap. This is an IO action that gets the user action to perform from the user. Then, use fromMaybe to set a deafult value (in this case id) to convert your Maybe UserAction to a UserAction:

getAction :: IO UserAction
getAction = fromMaybe id . getUserAction <$> getLine

Note that a . b <$> c is (a . b) <$> c, not a . (b <$> c).

Now you can use this function in one shot in your main loop:

gameLoop :: User -> IO ()
gameLoop initUser = displayPosition initUser >> getAction >>= \userAction -> gameLoop (userAction initUser)

Or use do-notation for the same thing:

gameLoop :: User -> IO ()
gameLoop initUser = do
  displayPosition initUser
  userAction <- getAction
  gameLoop (userAction initUser)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.