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)
foreverfromControl.Monad.