开发者

Haskell and State

开发者 https://www.devze.com 2023-01-19 23:54 出处:网络
Haskell is a pure functional programming language. My 开发者_Go百科question is: What are the advantages and disadvantages of using Haskell to solve problems involving lots of state, for example GUI p

Haskell is a pure functional programming language.

My 开发者_Go百科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.


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. There 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 PRNG 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).


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

  • The Yampa Arcade: http://www.haskell.org/yale/papers/haskell-workshop03/yampa-arcade.pdf
  • FRP GUI Toolkit: http://www.haskell.org/fruit/
  • Other Yampa Resources: http://lambdor.net/?p=44


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.


A state monad is about the worst way to model a GUI or a game in Haskell. I think the second best option is to use concurrency in both cases. However, the best option has been mentioned by Paul: Functional reactive programming (FRP).

Personally I advocate arrowized FRP (AFRP), which I think was first implemented as Yampa and later forked as the slightly more useful Animas. However, Yampa quickly reaches its limit, so I have written a more powerful, more expressive library called netwire, which also has some conceptual improvements over the former two.

In its essence AFRP is a functional state system. It is functional in that state is not modeled as changing variable values, but mutating functions. This is cleaner and does not require any imperative programming like state monads do.


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消