I'm James Hague, a recovering programmer who has been designing video games since the 1980s. This is Why You Spent All that Time Learning to Program and The Pure Tech Side is the Dark Side are good places to start.
Where are the comments?
Functional Programming Doesn't Work (and what to do about it)
Read suddenly and in isolation, this may be easy to misinterpret, so I suggest first reading some past articles which have led to this point:
Admitting that Functional Programming Can Be Awkward
Follow-up to "Admitting that Functional Programming Can Be Awkward"
Back to the Basics of Functional Programming
Purely Functional Retrogames
How I Learned to Stop Worrying and Love Erlang's Process Dictionary
After spending a long time in the functional programming world, and using Erlang as my go-to language for tricky problems, I've finally concluded that purely functional programming isn't worth it. It's not a failure because of soft issues such as marketing, but because the further you go down the purely functional road the more mental overhead is involved in writing complex programs. That sounds like a description of programming in general--problems get much more difficult to solve as they increase in scope--but it's much lower-level and specific than that. The kicker is that what's often a tremendous puzzle in Erlang (or Haskell) turns into straightforward code in Python or Perl or even C.
Imagine you've implemented a large program in a purely functional way. All the data is properly threaded in and out of functions, and there are no truly destructive updates to speak of. Now pick the two lowest-level and most isolated functions in the entire codebase. They're used all over the place, but are never called from the same modules. Now make these dependent on each other: function A behaves differently depending on the number of times function B has been called and vice-versa.
In C, this is easy! It can be done quickly and cleanly by adding some global variables. In purely functional code, this is somewhere between a major rearchitecting of the data flow and hopeless.
A second example: It's a common compilation technique for C and other imperative languages to convert programs to single-assignment form. That is, where variables are initialized and never changed. It's easy to mechanically convert a series of destructive updates into what's essentially pure code. Here's a simple statement:
In single-assignment form a new variable is introduced to avoid modifying an existing variable, and the result is rather Erlangy:
The latter is cleaner in that you know variables won't change. They're not variables at all, but names for values. But writing the latter directly can be awkward. Depending on where you are in the code, the current value of whatever "a" represents has different names. Inserting a statement in the middle requires inventing new names for things, and you need to make sure you're referencing the right version. (There's more room for error now: you don't just say "a," but the name of the value you want in the current chain of calculations.)
In both of these examples imperative code is actually an optimization of the functional code. You could pass a global state in and out of every function in your program, but why not make that implicit? You could go through the pain of trying to write in single-assignment form directly, but as there's a mechanical translation from one to the other, why not use the form that's easier to write in?
At this point I should make it clear: functional programming is useful and important. Remember, it was developed as a way to make code easier to reason about and to avoid "spaghetti memory updates." The line between "imperative" and "functional" is blurry. If a Haskell program contains a BASIC-like domain specific language which is also written in Haskell, is the overall program functional or imperative? Does it matter?
For me, what has worked out is to go down the purely functional path as much as possible, but fall back on imperative techniques when too much code pressure has built up. Some cases of this are well-known and accepted, such as random number generation (where the seed is modified behind the scenes), and most any kind of I/O (where the position in the file is managed for you).
Learning how to find similar pressure relief valves in your own code takes practice.
One bit of advice I can offer is that going for the obvious solution of moving core data structures from functional to imperative code may not be the best approach. In the Pac-Man example from Purely Functional Retrogames, it's completely doable to write that old game in a purely functional style. The dependencies can be worked out; the data flow isn't really that bad. It still may be a messy endeavor, with lots of little bits of data to keep track of, and selectively moving parts out of the purely functional world will result in more manageable code. Now the obvious target is either the state of Pac-Man himself or the ghosts, but those are part of the core data flow of the program. Make those globally accessible and modifiable and all of a sudden a large part of the code has shifted from imperative to functional...and that wasn't the goal.
A better approach is to look for small, stateful, bits of data that get used in a variety of places, not just on the main data flow path. A good candidate in this example is the current game time (a.k.a. the number of elapsed frames). There's a clear precedent that time/date functions, such as Erlang's
now(), cover up a bit of state, and that's what makes them useful. Another possibility is the score. It's a simple value that gets updated in a variety of situations. Making it a true global counter removes a whole layer of data threading, and it's simple: just have a function to add to the score counter and another function to retrieve the current value. No reason to add extra complexity just to dodge having a single global variable, something that a C / Python / Lua / Ruby programmer wouldn't even blink at.
(Also see the follow-up.)