Admitting that Functional Programming Can Be Awkward
prog21.dadgum.com
prog21.dadgum.com
My architecture (so far - it's in early planning stage): 2 threads (physic and logic thread going 10 Hz and graphic thread going as fast as it can drawing world)
objects are struct-maps with fields position, animation frame, life etc (all fields are functions of time, and can change between physic thread turns, but not in one physic thread turn).
Graphic thread just gets time and for each visible object draws it at current position (calculated from time by objects position function). So movement should be smooth and not dependent on frame rate.
physic thread in response to player input calculates new functions for objects (only for X next turns). These new functions are encapsulated in event objects, that are added to events priority queue.
On each turn (of physic thread) events for that thread are executed - each event takes set of game objects (not always all objects) and time and returns changed set of game ojects.
In C++ this will be much easier to write at first (no hard thinking needed:)), but later it could become nightmare to change because of side effects.
Modern computers are fast; Program it naively and if you see it's too slow (which it probably wouldn't be) optimize the bottlenecks.
I am pretty sure you wouldn't need to make this multithereaded.
The few languages that aren't overengineered are the ones that either have a very specific domain (JavaSript), slow to change standards (C), or are very new (Clojure).
OP's idea also doesn't sound very over engineered to me. The whole draw loop thing is simply a convention and not an obvious one. I've taught many a student who stumbled on the idea of the render loop because there's simply nothing natural or obvious about it.
In the real world everything happens at once, there is no update-render-loop. Snapshoting the state of a multithreaded physics engine at discrete intervals in order to render the game state is much closer to a model of how we perceive a fully concurrent reality.
What's for sure overenginered is using function of time instead of simple numbers for positions. But that is just to make it different.
That's one option: to admit that functional programming is the wrong paradigm for some types of problems. Fair enough. I'd put money on that. But it also may be that almost no one has been thinking about problems like this, that functional programming attracts purists and enamored students.
The language features that help with the problems small programs have are intrinsically easier to show off than those that help manage larger systems. Beyond a certain point, I don't care how easy something makes a program that will never grow beyond a page, though. (If that isn't easy, your language has serious problems!) I'm more interested in how these techniques / paradigms break down, not how well they work on examples chosen for them. You can't always choose your problems.
(And yes, one of the main things I've learned from Unix is that it's usually better to break large systems into a team of smaller components, but that's neither here nor there. Also, informed use of multiple paradigms is likely the best long-term approach, but that's not a position that necessarily translates well into sound-bytes.)
Of course, it's easier to hype languages that let you do short parlor tricks, particularly on web forums.
Also, he has a followup posted here: http://prog21.dadgum.com/4.html
Quite the opposite - functional programming is becoming very appealing as a practical alternative for building large, multi-threaded, correct systems.
True, it might be a bit more tedious to write a program that type-checks, but as many (including me) have been repeatedly amazed, it often works correctly right away, with minimal debugging.
But it also may be that almost no one has been thinking about problems like this
Mutable state is a fundamental issue that every functional programmer deals with every day. Indeed, it might be sometimes frustrating to pass around state that in C would be directly modifiable, but the benefits are huge.
Pure code has been called a "scarce resource", and that's exactly right - once it works, it will always work, no matter how huge is the system you build on top of it.
BUT, pure functional programming is an intrinsically poor fit for some problems, and this is seldom addressed by hype. I think functional programming hype needs a reality check now, the same way C++-style OO hype could have used more way back when. I think we would all be better off if, instead of heralding it as the next technique that solves all problems, we work on getting a clearer idea of its strengths and weaknesses.
But I somehow cannot see any functional programming hype. Where are the people heralding it as solving all problems? Even in a place like HN with a highly non-average community of programmers, the FP links are quite rare and very intro-level. I would say FP is a high-hanging fruit that's only starting to find a few niches in real-world programming.
So I son't see anything close to an FP hype, nowhere near to the Java hype or Ruby hype.
Also, OCaml is sometimes called a "higher-order imperative programming language" instead of a functional programming language. It does have strict evaluation and first-class mutation, after all. I would argue that there is NOTHING awkward about writing code in it, controlling for libraries and development tools.
I think it'd help if there were more books on OCaml. The French O'Reilly one is pretty good (and free online, http://caml.inria.fr/pub/docs/oreilly-book/), but the Joshua Smith one is awful, and there aren't many others in English.
Also, the OCaml syntax scares some people, and its error messages are pretty unclear, particularly if you haven't learned how to work with the flow of H-M typing. These (and other similar issues) aren't fundamental problems with the language, though, just rough edges in the implementation.
In OCaml, I can create a module with multiple types, hide information about both of them, and write code sees both of their privates at the same time. In OOP, this can't be done as elegantly (only through the use of inheritance, friends, or inner classes). And this is just the tip of the iceberg; there's a lot more flexibility.
Books would help, as would improving the quality of the error messages (which are literal translations from French). However, neither of these are problems with the language itself... you can improve them without changing a single line of code or iota of the specification.
Type error messages will with HM always be hard (I think understanding an OCaml error message is a PSPACE-complete problem). But, Haskell has great type error messages, so again, this can be improved.
However, I still stand by my claim that the language itself doesn't force programs to be awkwardly structured, as is claimed throughout this discussion.
Such as? Ok, so you get better multithreaded behaviour but now you're copying state all the time and having to thread it though all your functions. Sometimes just updating state is the right thing to do.
Personally, I think a good tradeoff is immutable data but mutable references.
If you have a function that moves a player in a game such as movePlayer :: PlayerState -> PlayerState, you know that it only changes the state of this player, and nothing else. In an OO language you would do something like player.doUpdate(), which has absolutely no guarantees.
Plus there are many facilities available to deal with threading and hiding state.
This gets ugly when the state of every actor can potentially affect the state of every other actor, though. This is pretty common in games.
That's the main point the linked article is making.
My point, instead, was that FP does not break down when things get ugly; on the contrary, it gives much stronger guarantees on correctness. Of course, it takes effort to express the problem in the more restrictive functional paradigm.
Maybe we can call your thesis "verbosity is power".
A page of COBOL is easy to understand and easy to write; a page of APL is much slower to understand and fiendishly difficult to write. That doesn't mean you should prefer COBOL over APL. It's a result of the fact that you can express more in a line of APL than in a page of COBOL. And, in fact, that line of APL is easier to write and easier to read than that page of COBOL.
Ideally, any time you're solving a problem that's a small variation of a problem somebody has already solved, you should be able to write your program by referencing their solution and describing the variation. In such a world, no program is more than a page.
There are diminishing returns in focusing on extreme brevity. If I'm going to write a script, I just use Lua. (Other people like Python or Ruby. Sure.) Squeezing 10% more out of an already small program is well past the point of diminishing returns.
Depending on the problem, APL/J/K might make more sense. Or awk, make, SQL, etc. (My only experience with the APL family is playing around with J a bit, but they seem like awk in that they gain their conciseness in exchange for being focused on a certain problem domain. That's usually a good trade-off, though.)
I'd rather see more focus into techniques to make larger projects manageable, like module systems, type inference, declarative programming. As techniques, they're intrinsically harder to show off in this medium, though (except for small amounts of type inference, but its payoff increases with project size). That's all I'm saying.
Try looking at it from this perspective: There is really only one program. It is running on the internet and the disconnected nodes that only exchange data by sneakernet. It never stops running. Every bit of software that we think of as a separate program is really a part of this program. What are the techniques that make this program manageable (in the sense that it continues to perform useful functions and people continue to extend it)? I don't think they're module systems and type inference. I think REST, for example, plays a much bigger role. Full-text search of documentation plays a big role. NANOG plays a big role. Most of the things that really matter most in keeping it manageable are interpersonal and institutional rather than purely technical.
But this large program is made up largely of code, and nearly all of that code is made up of individual lines of code. Many of the technical things that help us most to keep it manageable are precisely the ones that allow us to write self-contained modifications more and more incrementally — a page, or a paragraph, or a line of code at a time.
So in that sense, things like type inference and programming higher on the declarativeness scale (e.g. DSLs, libraries) are important in managing the large project precisely because they can be compellingly demonstrated in "programs" of less than a page.
Also I don't think I heard any serious "purists" lately. Aren't we fighting windmills here?
(The usual pattern is "transform :: world state 1 -> world state 2". The transformer can "modify" as much of state 1 as it wants.)
How is passing around world state from one transformer function to another an improvement over traditional stateful programming where functions change variables?
Though it would be interesting hear how people who have been doing FP for a while handle it.
"Premature optimization is the root of all evil."
Computers are fast. On my machine, I can allocate, initialize, and garbage collect almost 4G of memory a second. This sort of thing is really fast, and is unlikely to be a performance problem.
When all of your data is immutable, however, you can reuse the data that you didn't change. The canonical example is a binary tree consisting of 2^n nodes. Changing one node only requires you to allocate n new nodes. As your tree gets bigger, the copying overhead goes to 0.
In the case of Haskell, the compiler is smart enough to figure this all out for you. So all you need to do is write correct code, and let the compiler make it work on a machine.
(It's also theoretically possible to reuse structures by mutating them instead of collecting-and-reallocating. You don't do that as part of your game, though, you let the compiler's optimizer do it. It will do it right, you will do it wrong.)
When everything has to happen in 1/60th of a second, then all that garbage collection is a serious problem. Memory allocation and deallocation is expensive in games, especially on the consoles.
For simpler games, yes, not a problem.
But the problem is valid. In games every nanosecond counts, which is why the control manual memory allocation offers is so attractive.
It's a problem that even small, single-unit game developers have. On the indie game dev forums, fighting against the .Net/Java GC is a common topic. When the GC kicks in, your framerate usually suffers and you waste productivity with tricks to avoid triggering it. When you have multiple cores, asynchronous GPUs and multi-threaded OSes it affects anything other than the most basic of games.
Having said that, in http://lambda-the-ultimate.org/node/1277 Tim Sweeney (of Epic, developers of Gears of War and the Unreal Engine middleware) says:
"Will gladly sacrifice 10% of our performance for 10% higher productivity" and also "Garbage collection should be the only option"
That presentation is a good summary of the current situation of AAA-games programming (well, three years old, but still) and why Epic are programming in C++ and not Haskell or some other functional language, even though they'd like to. Well worth a read.
(let ((state 'someobject))
(defun phase1 ()
(done1 state))
(defun phase2 ()
(done2 state)
.
.
.and so on
Programming in Lua has a nice example of a maze game where state is passed around by emitting functions. It's a toy problem, but I think it's instructive: http://www.lua.org/pil/6.3.html
The OP is accustomed to thinking only in terms of mutating state. He can't get away from it. He wouldn't have had all of these problems if he'd just done ye olde AI class with turn-based board game project in Lisp. (With a prof who set a coding standard for pure functional Lisp.)