Do you have anything that you want to do, but discover halfway through for some reason it's not legal? Good luck undoing what was half done. Of course you might think the simple answer is "don't do that", but as the game gets bigger and responsibilities get spread out between all the objects it becomes increasingly easy for some action to become possible that requires a lot of checking, and if things are just willy-nilly mutating themselves the odds of you encountering this sort of problem skyrocket. And that's just one example... games by their nature resist the sort of decomposition that most "real" software can have done on it, because as soon as you tell a designer that X can't talk to Y, by golly they'll start thinking about how awesome it would be if X was talking to Y. You don't get a pretty tree of relationships with occasional crosstalk, you get a nasty snarl of communication patterns, irreducibly.
Many games end up functional under the hood anyhow, with a clearly-defined loop:
1. Ask all the objects in the world what they "want" to do.
2. Validate and resolve conflicts.
3. Render the new world state.
One of OO's big problems in general is lack of control over the profound, often-overpowerful effects of mutation, and used in an undisciplined manner, games often turn into a worst-case-scenario for OO. And most of using them in a "disciplined" manner turns out to be using "FP" techniques to keep the complexity under control.Of course nothing stops you from doing that in OO, with discipline and care unenforced by the compiler. But what it does mean is that suddenly the "mismatch" that you're referring to disappears, and one starts wondering whether perhaps FP is the better match for games and OO the accident of history.
So if the two issues are orthogonal then you're both right. :-)
Where is the OO in that approach? The encapsulation?
The code is right there on GitHub, although if you're not familiar with Haskell (and lens), it might seem cryptic. But you'll see that it is actually mostly written in an imperative style. The logic is mostly pattern matching on events to generate record updates. This is a common style in Haskell and fits the domain well.
There are other more elegantly value-oriented ways of writing simulations in Haskell; look up functional reactive programming.
Then you define a function that is like (in C-ish):
GameState tick(GameState current, Action playerMove) {
... set up the next frame and return it ...
}
And a function to play like: void play() {
play(initialState());
}
void play(GameState current) {
if(current.gameOver()) return;
Action playerMove = getPlayerMove();
GameState next = tick(current, playerMove);
play(next); // In Haskell and C on higher optimizations, this tail recursive call will get compiled to a loop
}
Of course it can get a bit more complicated depending on the context, but you get the idea.So I'd say functional programming is neither better nor worse. A lot of people get hung up on the idea of immutability, but really I've found it to be a non-issue.
That is just one way to look at it. You could also say that they can all be described by the universe's state at a point in time. Then, you'd have a function that takes the old state and some actions to produce a new state, by recursively doing the same to each object within the universe.
That's an interesting idea (and might fit in with "math / science gamification" software). Terry Tao used the opposite point of view, describing quantum mechanics in terms of video games: http://terrytao.wordpress.com/2007/02/26/quantum-mechanics-a....
It skips a little in the beginning, but I think you might find it interesting.
[1] http://research.microsoft.com/en-us/people/smcdirm/managedti...
Elm code is only pure in that it doesn't have implicit side effects, it definitely has state.
However, if you have deterministic execution, you can always just re-execute from the beginning to get the same execution. This works for functional and non-functional code.
Is deterministic execution the same as referential transparency?
I think what you are saying is that you get the benefit of a functional language when you use a functional style regardless of the language.
Why would you save versions of everything? That sounds like a pessimistic strategy that you would need in a stateful environment where anything can be destructively updated at any time, which doesn't apply in this case.
> , or build a programming model especially suited for it [1].
Sorry if I don't take the claim "especially suited for it" at face value, seeing as you are the co-author and you are criticizing something which seems to try to achieve the same thing in a different manner. Maybe that's unfair of me.
OO takes the divide and conquer approach, by dividing state into progressively smaller chunks until each piece is small enough to reason about. The flaw with that is that at some level you need to understand and reason about the entirety of the state, and if you have a bug in some lower level piece it often exhibits as bad behavior in one of the higher level pieces. Figuring out which of the lower level pieces is ultimately responsible is 90%+ of the time spent on debugging in a OO program. Another challenge is that because state is mutable, just finding the starting point to debug the problem is often a chore because you must first get the system into the state necessary to trigger the bug. Even worse is that often the state you initially detect the bug in, is actually significantly further along than where the bug originates so you end up having to attempt to progressively walk backwards through time to arrive at the originating point (which in practice means running a debug session to a point, finding the next piece of state that looks wrong, setting new breakpoints, and then repeating the whole exercise).
Functional programming takes a different tack by instead strongly discouraging state altogether. A well written functional program will contain only the absolute bare minimum state necessary to accomplish the task at hand. Further state tends to be divided into two distinct camps, the global state that's referenced by name (largely non-existent barring mutable IO refs and such), and the local state that's passed explicitly as function arguments (ignoring for the moment the monad abstraction around that). Like with OO you sometimes arrive at a point where behavior isn't what you expect it to be and need to go to a debugger to figure out where the state has deviated from the expected. Unlike in OO however you have the call stack to reference and can see exactly where in the history the problem originated. Finding the source of a bug in a large functional program tends to be rather trivial compared to the similar task in a large OO program, due to the more concise and limited state that exists in functional programs.