views:

313

answers:

4
+5  Q: 

Haskell and State

Haskell is a pure functional programming language.

My question is: What are the advantages and disadvantages of using Haskell to solve problems involving lots of state, for example GUI programming or game programming?

Also a secondary question: what methods are there to handle state in a functional way?

Thanks in advance.

+7  A: 

While I do not doubt that people will be answering with "use the state monad", I'd like to point out another useful method: Functional reactive programming (with Yampa or otherwise).

Paul
+7  A: 

I'm going to answer your second question first. There are actually many ways to handle mutable state in Haskell (and other FP languages). First of all, Haskell does support mutable state in IO, through IORef and mvar constructs. Using these will feel very familiar to programmers from imperative languages. here are also specialized versions such as STRef and TMVar, as well as mutable arrays, pointers, and various other mutable data. The biggest drawback is that these are generally only available within IO or a more specialized monad.

The most common way to simulate state in a functional language is explicitly passing state as a function argument and returned value. For example:

randomGen :: Seed -> (Int, Seed)

Here randomGen takes a seed parameter and returns a new seed. Every time you call it, you need to keep track of the seed for the next iteration. This technique is always available for state passing, but it quickly gets tedious.

Probably the most common Haskell approach is to use a monad to encapsulate this state passing. We can replace randomGen with this:

-- a Random monad is simply a Seed value as state
type Random a = State Seed a

randomGen2 :: Random Int
randomGen2 = do
  seed <- get
  let (x,seed') = randomGen seed
  put seed'
  return x

Now any functions which need a PSRG can run within the Random monad to request them as needed. You just need to provide an initial state and the computation.

runRandomComputation :: Random a -> Seed -> a
runRandomComputation = evalState

(note there are functions which considerably shorten the definition of randomGen2; I chose the most explicit version).

If your random computation also needs access to IO, then you use the monad transformer version of State, StateT.

Of special note is the ST monad, which essentially provides a mechanism to encapsulate IO-specific mutations away from the rest of IO. The ST monad provides STRefs, which are a mutable reference to data, and also mutable arrays. Using ST, it's possible to define things like this:

randomList :: Seed -> [Int]

where [Int] is an infinite list of random numbers (it'll cycle eventually depending on your PSRG) from the starting seed you give it.

Finally, there's Functional Reactive Programming. Probably the current most prominent libraries for this are Yampa and Reactive, but the others are worth looking at also. There are several approaches to mutable state within the various implementations of FRP; from my slight use of them they often seem similar in concept to a signalling framework as in QT or Gtk+ (e.g. adding listeners for events).

Now, for the first question. For me, the biggest advantage is that mutable state is separated from other code at the type level. This means that code can't accidentally modify state unless it's explicitly mentioned in the type signature. It also gives very good control of read-only state versus mutable state (Reader monad vs. State monad). I find it very useful to structure my code in this way, and it's useful to be able to tell just from the type signature if a function could be mutating state unexpectedly.

I personally don't really have any reservations about using mutable state in Haskell. The biggest difficulty is that it can be tedious to add state to something that didn't need it previously, but the same thing would be tedious in other languages I've used for similar tasks (C#, Python).

John
+1  A: 

Normally what you would do is use a Monad Transformer with an StateT and a IO, this because the view (GUI) needs IO in order to respond, however once you defined your Monad Transformer in a newtype, you would like to make the signatures of the game logic only with the MonadState interface, that way you still have the benefit of non-IO-ness changes. Code below explaining what I mean:

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
import Control.Monad.State

data GameState = GameState { ... } deriving (Show)
newtype GameMonad a = GameMonad (StateT GameState IO a) 
                      deriving (Monad, MonadState GameState, MonadIO)

-- This way, now you have a monad with the state of the info
-- what would you like now is being able to modify the state, without actually
-- having IO capabilites.

movePlayerOnState :: (MonadState GameState) m => Position -> m ()
-- In this function we get the state out of the state monad, and then we modify
-- with a pure function, then put the result back again

-- Other times you would like to have the GUI API available, requiring the IO monad
-- for that
renderGameFromState :: MonadIO m => GameState -> m ()
-- in this method you would use liftIO method to call the GUI API

This code is fairly complex if you don't understand monads, but my rule of thumb is, find out what the State Monad is for, understand what Monad Transformers are (without the need of understanding how they work) and how to use the StateT monad.

I can point you to a Sokoban project I did with other teammate that might be useful, it uses ncurses as the GUI, but you can get the idea of the logic and how we managed states on the Game

http://github.com/roman/HaskBan

Good Luck.

Roman Gonzalez
+1  A: 

What are the advantages and disadvantages of using Haskell to solve problems involving lots of state, for example GUI programming or game programming?

The advantage is that, even if you're not particularly taking advantage of purity, Haskell is simply a good language.

First-class functions — shouldn't be a big deal in 2010, but it is. Algebraic types with pattern matching. Powerful static type checking with type inference. Clean syntax. First-class concurrency, STM, and thread-free pure parallelism. A good compiler. Tons of libraries and more every day. An active, helpful community.

These aren't big ideological decisions like purity or laziness. They're just good ideas. They're things most languages could have, but too many don't.

keegan