
Hi Paul, You gave some suggestions of other styles of Haskell programming that Ronald could try for his program. These styles are definitely worth knowing, so if Ronald is not familiar with them, he may want to try them out. However, in most cases, I think what Ronald already did is nicer than what you are suggesting. Paul Johnson wrote:
The design reads very much like a straight translation from the imperative style, which is why so much of it is in the IO monad. There is nothing wrong with this for a simple game like Hangman, but for larger games it doesn't scale.
It's a state monad, and most of his code is in that style. It doesn't read to me like imperative style at all. And it scales beautifully. There is a lot of liftIO, because that is the bulk of the work in this program. But Ronald cleanly separated out the game logic, and even the pure parts of the UI logic, into pure functions like "handleGuess" and "renderGameState". I personally might have kept a more consistently monadic style by writing those as pure monads, like: handleGuess :: MonadState GameState m => Char -> m () renderGameState :: MonadState GameState m -> m String In certain situations, that approach gives more flexiblity. Like for refactoring, or adding new features. But Ronald's approach is also very nice, and I might also do that.
1: Your GameState type can itself be made into a monad.
Yes, it can. But StateT GameState IO is the perfect monad for this game - writing a new monad would just be reinventing the wheel. It would certainly be a good learning experience for understanding the inner workings of the state monad transformer, though.
2: You can layer monads by using monad transformers. Extend the solution to part 1 by using StateT IO instead of just State.
I think he is already using that type.
3: Your current design uses a random number generator in the IO monad. Someone already suggested moving that into the GameState. But you can also generate an infinite list of random numbers. Can you make your game a function of a list of random numbers?
He could, but I would advise against that technique. In more complex games, you may need to do many different kinds of random calculations in complex orders. Keeping a random generator inside a state monad is perfect for that. And since Ronald already set up the plumbing for the state monad, he is already home. Generating an infinite list from a random generator "burns up" the generator, making it unusable for any further calculations. Sometimes that doesn't matter, but I think it's a bad habit. I admit you'll catch me doing it sometimes though, in "quick and dirty" situations like at the GHCi prompt.
4: User input can also be considered as a list. Haskell has "lazy input", meaning that you can treat user input as a list that actually only gets read as it is required. Can you make your game a function of the list of user inputs? How does this interact with the need to present output to the user? What about the random numbers?
That type of "lazy IO" is considered by many to be one of Haskell's few warts. It is a hole in the type system that lets a small amount of side-effects leak through, and even that small amount leads to bugs. In many situations it's hard to avoid without making a wreck out of your whole program structure (though more and more tools are becoming available to help, such as the ByteString stuff). Ronald did great without it - why resort to that? All that said - this is clearly a matter of taste, of course. Thanks for bringing up a variety of approaches. Regards, Yitz