3 ms·
Chapter 3 of The Structure and Interpretation of Computer Programs is a great discussion of mutable state (make sure you read the previous chapters, though, or
by euccastro 19y ago
Chapter 3 of The Structure and Interpretation of Computer Programs is a great discussion of mutable state (make sure you read the previous chapters, though, or at least the first chapter and The Little Schemer).
A functional-ish way to run a game world simulation:
(define (run-game)
(define (make-initial-world) ...)
(define (get-input) ...)
(define (wait-for-next-frame) ...)
(define (update-world world input) ...)
(define (game-over? world) ...)
(define (run-step world)
(display-world world)
(wait-for-next-frame)
(if (game-over? world)
'bye
(run-step (update-world world (get-input)))))
(run-step (make-initial-world)))
It's not purely functional: the calls to display-world and wait-for-next-frame are made for their side effects. But they are not mutating any program state, so this is good enough. You _can_ try and get more 'pure' with Haskell and monads when you feel brave, but there is plenty to grok before going there.
Let's study the parts:
make-initial-world: self-explanatory; I'll only add that this object is held 'in the stack' and (conceptually) never overwritten.
get-input returns the player actions, game time, and whatever other 'delta' data is relevant for the current update. You can think of the input over a whole game session as an indefinitely long list which you iterate over, one element (game update) at a time. You can indeed run automated tests replacing this data with an actual list; the program shouldn't notice the difference.
wait-for-next-frame causes a time delay that is conceptually irrelevant to the rest of your logic (you deal with system time, if at all, in the body of (get-input)). In test situations, you should be able to just replace this with a no-op.
game-over? tells you whether the player has quit. I make it a function of the world and not the input because the input is meaningless without the context that the world object represents.
(By this time you may have noticed that I'm being a bit sloppy with terminology, for color and simplicity. 'world' is not only the game world, but also the program state (e.g., is the user in a configuration screen?). Similarly, 'game over' is not the end of one game/'match' but of the playing session. The principles are the same.)
run-step is just a loop that keeps updating and displaying the world state until the game is over. Don't be scared by the recursive call to run-step itself; that's the idiomatic way to loop in Scheme, and implementations are required to handle it efficiently (google for 'tail recursion').
The crux of the problem is in update-world. This is an ordinary function that takes the world and the input for this game tick, and returns the world corresponding to the next tick.
You'd think there are only two options here: either modifying the existing world in place- definitely non-functional!-, or building a new one from scratch, which sounds inefficient.
Actually, there is much more to be said about purely functional data structures. In a nutshell, you can often make a new world that shares most of the data with an old one, so only the delta is created anew. References to the old world will not be affected at all by the new one, so you can keep as much history of your game as you have memory for. As a basic example, in Lisp you do this with lists all the time. But it can get more complex:
http://www.cs.cmu.edu/~rwh/theses/okasaki.pdf http://www.cs.cmu.edu/~rwh/theses/okasaki.pdf
(This thesis was expanded upon and published as a book by the same name, with examples in ML and Haskell. If someone knows of more resources about this, please let me know.)
Depending on the ratio of changing-data-per-frame versus total data in your application, this may be an interesting option. The learning curve is considerable, though.
But note that when update-world is called the old world will never be used again (most games don't keep the state of every past frame). So in this case it's OK to modify the old world in place and use that as the return value. The rest of the code doesn't care; again, you might as well be iterating over a stream of worlds (with the same desirable implications for testing).
If you do mutate the world, though, you may want to make it explicit by saying something like
(define (run-game)
...
(define (run-step)
(update-world! world (get-input))))
(display-world world)
...)
(define world (make-new-world))
(run-step))
This is harder to test and more work if you want to switch to a purely functional update-world later, but there's less risk you or others will try and use (or modify!) a world after it's been updated.
Part of the calculations _within_ update-world can still be done in a functional way, and where you need to mutate state, you can often hide that behind a functional interface, with varying degrees of hygiene.
The bottom line is you don't need religious avoidance of mutable state to benefit from functional programming at all; you strive for stateless code by default, you compromise where it makes sense, and you try to minimize the impact of such sins by wrapping functional interfaces, or placing warning signs, around them. The mere habit of paying attention to these matters will improve your taste.
There is a lot more to be said about this, but I hope you can already see how this is not an easy topic to introduce, out of the blue, via IRC. In all fairness to the Lisp folks, I have to say the question about Python packages was much more concrete and easier to address without both sides losing patience.
- euccastro 19y agoArgh! Anyone knows if/how can I introduce a pre block?
- palish 19y agoGreat info, great detail. Thanks so much for your time. I've never even considered looping through recursion. The construct-a-new-world vs modify-the-current-world situation was exactly the problem I was thinking of. And you followed it up with a paper, if I wanted to learn more. It's hard to emphasize how perfect stuff like this is for a newcomer to Lisp. There should be more snippets like this. You should write a tutorial somewhere.
- henning 19y agoScheme has kind of a different culture from Common Lisp. Most of the material is oriented towards students in order to teach the timeless stuff in computer science. I learned C before I learned any functional languages and I regret it to this day. It took me _years_ to undo the damage. I really dug myself into a hole, too, by learning all about pointers and low-level stuff like that before I ever knew what Python, Ruby, etc. were.
- euccastro 19y agoWhy, thanks! I myself reinforced some concepts while writing it, since I have more of a Python background. I like the idea of writing a tutorial about this, but I'd rather experiment more first.
- 19y ago