I'm still working on MazesOfMonad, my Haskell RPG, now on code.haskell.org. It's actually fun to now have a working program and think about extensions in functionality, and then how to implement them in Haskell, instead of just playing around with the language.
I wanted to give the user more feedback that they currently got about what was going on in the game, and at the same time I wasn't happy with what I had written previously: I had the output messages as part of the method signature:
myFunction :: Type1 -> Type2 -> (Type3,[String])
So I decided a Monad was called for. The Writer Monad, in this case, which lets us append stuff together to get it all back at the end. I already had most of my game running in a monad transformer, so it was **just** a matter of figuring out how to wrap everything in a WriterT. I had a bit of fun at first, then got it working. So now my game runs in a
WriterT a (RandT (StateT b IO)) c
Where a b a are various types for the things I need: a is the type of messages I write, b the state I keep, and c the return value. RandT gives me random numbers, StateT gives me a state, IO allows me to do IO operations when I need to save the game or something like that.
The amazing thing, well, for me, is that a lot of my functions do not need to be aware of the whole transformer stack, but of only the type classes that they care about. My final transformer is an instance of both MonadWriter and MonadRandom. So my method that needs to write messages becomes:
myFunction :: (MonadWriter [String] m)=>Type1 -> Type2 -> m Type3
And I can write my code calling the MonadWriter methods to add messages. It's in a monad, but it's still purely functional: I return a Type3 object and write Strings. If my function needs random numbers:
myFunction :: (MonadRandom m, MonadWriter [String] m)=>Type1 -> Type2 -> m Type3
and I get random numbers capabilities!
The final code is now much cleaner than before (I actually don't use [String] directly for the MonadWriter type but an alias for it, so I can replace it with Seq String or something faster the day I fancy it). I think I now understand monads and monad type classes better, and I understand better how using a monad doesn't automatically mean using the IO Monad and lose purity. Haskell Nirvana is not far off, says he naively...
2 comments:
I hate when my monad stacks have IO at the bottom. It just feels like I'm doing something wrong, although it's always "easier" to write the code that way.
But I find I feel much better when I explicitly list all the I/O operations I expect my code to do. Take a look at Control.Monad.Prompt on hackage and see if you can't put (Prompt GameIO) as the bottom monad in your stack. (You define GameIO yourself, of course)
This way, you can build tests for your game code that do not rely on IO at all; the code that would do IO is now part of the "runPrompt" for Game, and you make the dependencies of that I/O explicit.
Hello, thanks for the tip. As I said, only some functions use the full stack of transformers, because they need to do IO to save the game state to disk (after every operation the game is saved), but most ancillary functions use type classes to define which part of the stack they use.
Post a Comment