Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

After doing functional programming for a while now (6 years of Elixir) it seems obvious to me that it is possible and no more complex than using imperative logic. In fact I would go so far as to say that managing complexity (i.e. state) is much easier to do in a functional style than imperative.

Functional is all about transformation of data structures, and a game is nothing more than a function that takes {state, mouse, keyboard} as input and returns {new_state, output} where output is a set of Vulkan commands, or pixels that you bitblt to screen. Rinse and repeat in an vsynced infinite loop.

I think the linked articles above betray the fact that they were written 15 years ago when functional programming was a niche quasi-academic idea and no one was used to building stateful programs with it. 15 years ago it was all about Java and C++. Lisp was as forgotten as it is today. In 2024 most above average programmers have experience in functional logic, and it is not that arcane of an idea. The reason that it is not widely adopted is that functional programming is not as performant as imperative semantics, and you need all the speed you can get. On the other hand, imperative often means buggy mess as game complexity grows and painful debugging session trying to understand who and why changed this state variable.



Could an expert strong man the argument that functional programming languages like Haskell could in theory be more performant than C because of the nice properties of pure functional languages?


CPUs are imperative and pervasively mutable. Languages like Rust are able to have so called zero-cost abstractions that are reminiscent of functional programming (iterators for example) but the thing is abstractions are by nature always slower than the real thing.

But not all games are AAA FPS that require ingenious optimized codepaths to perform well, not in this day and age, so this is why you see commercial games running on "slower" abstractions such as C# (AOT compilation doesn't make it less of an abstraction layer), so you can very well design a commercial game in Haskell or Scheme or Lisp or Clojure if you wish. Nothing stops you, apart from the lack of serious game dev frameworks built in those languages.

It all depends on the compiler, really, and it is asymptotically hard to compile functional languages so they perform as fast as C for example. There is no market for "high performance Haskell or Clojure", so there is no compiler that good either.


"Cost" in C# comes from the (ab)use of classes for transient data, virtual/interface calls and automatic memory management. Another source of difference is in compiler capability of Unity's own flavours: IL2CPP, Mono and Burst, versus .NET's CoreCLR JIT and NAOT versus GCC/Clang. However, the latter has much less impact on "application code" and more impact w.r.t loop autovectorization, for which in C# you're supposed to use bespoke portable SIMD API which is fairly user-friendly. For the former, you are not "locked" into a particular paradigm like with Java or Rust and can mix and match depending on how hot a particular path is (for example - construct buffer slices with LINQ because there are only 16 of them, but each one is 512MiB large, so do the actual work with Vector<T>).

JVM languages have a huge gap* in low-level capabilities where-as C# sits next to C and Rust in many of the features that it offers, even if the syntax is different. JVM implementations also come with significantly higher FFI cost. This makes them an overall poor choice for game development. Your experience of writing a game engine in pure C#, calling out to rendering and other device APIs will be massively better than doing so in Java/Kotlin/Clojure/etc, because of both runtime capabilities and ecosystem of interop libraries.

Also, C# has zero-cost abstractions in the form of struct generics which are monomorphized like in Rust, and performance-sensitive code relies on this where applicable in "modern" codebases.

* Projects like https://github.com/bepu/bepuphysics2 are impossible to implement on top of JVM without calling out to native components. This might change once the incubating Panama vectors improve upon their API and what they compile to.


GHC is effectively an attempt to strong-man that argument over a couple of decades, and in short, it failed. It has pretty good performance for what all Haskell is doing, but if you want to write C-speed Haskell you are restricted to a tiny portion of Haskell that you can only understand with deep knowledge of GHC, and based on what I've seen of it, it is completely unrealistic to call it "Haskell" anymore. It's more a language that happens to be embedded inside of Haskell, but unspecified. (Sort of like "the performant subset of Javascript if you want the JIT to do its best"... it exists, but it's undocumented, it isn't the same between engines, and it's very hard to write it without an intense knowledge of the innards.)

The "sufficiently smart compilers" turn out to either not exist, or be beyond the ability of even the smartest humans.

This is not celebration of that, or condemnation that anyone tried. It's a major bummer, actually, and I am suitably bummed. I'd love to have the Sufficiently Smart Compiler. But wanting doesn't count for much. At this point if someone wants to argue that something at a Haskell level of "functional programming" can run at C speed routinely, they need to produce the compiler; we ran the gamut on mere theories.

(I have to qualify it that way because we do have a lot of evidence that you can have "functional flavored" languages that run much more quickly, like O'Caml (at least in single thread) and Rust, if you consider that "functional flavored". But straight-up Haskell does not appear to be able to "just" get transformed to C-speed code reliably.)


Only in some very specific situations, which amount to an arbitrarily smart compiler and arbitrarily dumb C code. Haskell does provide more opportunities for the code to not do something you tell it do because it can prove you don't use the result, but it is a dumb programmer who is writing code to do those unneeded calculations, and thus a good programmer would eliminate them from C.

A lot of effort has gone into optimizing C compilers, Haskell is easier to optimize in theory, but in practice it isn't enough easier as to overcome the massively larger amount of effort put in C.

In the best case for both the code will be roughly the same speed. However the best case for both can look very different and any comparison is suspect as it is likely that whoever wrote the code wasn't as good at one as the other and so wrote bad code.


C is basically portable assembly so handwritten C can be as fast as the hardware can allow.

High-level languages like Haskell can and do approach the performance of handwritten C if you encode enough information to get the optimizations for free:

https://stackoverflow.com/questions/35027952/why-is-haskell-...


>a game is nothing more than a function that takes {state, mouse, keyboard} as input and returns {new_state, output}

but that's a completely convoluted approach to identity in a model for a videogame. The reason Rich Hickey took that approach in Clojure to state and identity is because in the domains he cared about he wanted to prevent the destruction of past state. (he wanted to avoid what he called "place based programming" IIRC).

If you want to implement a git like versioning history say, you really care about any change to state as a collection of individually immutable snapshots. But in a videogame that's completely artificial. Nobody thinks about a persistent entity in a game as a collection of states at a million points in time. It's much more natural and performant to think of your entities as persistent and mutable and you basically only care about where they are now.


> you basically only care about where they are now.

You care about what they are doing now, and what they should do on next render based on the rules of the game and the player’s input. To me, that looks a lot like (state, actions) => newState.

I’m not sure how much of a difference there really is between having objects track their own state vs having them pull their state from somewhere in a single State tree. The advantage to the latter approach IMO is that it simplifies cross-cutting concerns (“player deals bonus damage when all enemies are affected by poison”), and it helps immensely with debugging to be able to query the exact, full game state whenever it’s needed.

My brain has been rewired to find FP easier to reason about than OOP. This won’t be true for everyone, and it isn’t always the right tool. I think it works well for game dev but can be held back by performance concerns.


It's not that artificial, it's basically a traditional game loop where mutation happens in the background between iterations. It has many of the same challenges too.


Coming from clojure I'm very wary of any non-mainstream languages like Elixir, Rust, Clojure etc.

It's a costly business mistake to build your tech stack using any of the above languages. The trade off simply is not significant enough to matter vs using mainstream languages with 10,000x the number of mindshare.


The cost is not paid today, but in the future when whatever you choose is no longer supported. If you write in something like C++ or Java running on Linux or Windows I'm confident that in 20 years you will find a tool that can build your code for the latest computers (not just the compiler, but the other tools as well). Of course Python 2 is a perfect example of why even the highest confidence choices may be wrong.

Rust is starting to look like it will make the jump to likely to be around for the foreseeable future, but only time will tell.

Of course just because you can build it doesn't mean it will work. I know of an embedded system where the 16 bit CPU went obsolete and they discovered that the C code was not 32 bit safe and worse it relied on the timing of the one CPU it was written for (though not so bad as to have to turn off compiler optimizations). I know of a product written for X11 that is porting to wayland. Keeping code running for decades is a hard problem - and one that many fail to understand. (nor do they need to - the web site you do today will need a major rewrite to fit the latest UI fad no matter what you do)


The other two languages GP mentioned, Elixir and Clojure, run on the BEAM and Java virtual machines respectively. I can’t speak to BEAM/Erlang, but your confidence that the JVM isn’t going away should remove any concerns about Clojure being supported in 20 years. It’s just a Java library, and has the same conservative approach as Java of ensuring backwards compatibility.

Clojure won’t necessarily live as long as the JVM does, in that the language could someday be abandoned, but support for its hypothetical last language version won’t be somehow removed from the JVM.


> support for its hypothetical last language version won’t be somehow removed from the JVM.

Why not? Python2 is a good example - if they make a JVM2 without the cruft and then a transition they will leave behind those that don't transition. Of course that is the worst case and I'll admit unlikely.

(also the JVM isn't very relevant to me because I work in embedded systems without a JVM)


You wrote above:

> If you write in something like C++ or Java running on Linux or Windows I'm confident that in 20 years you will find a tool that can build your code for the latest computers

What I wrote was based on your confidence in Java (the language) being around in 20 years. One of Java’s core features is backwards compatibility. Clojure’s implementation will continue to work as long as Java (the language) itself exists. There’s too much business depending on old Java software for the language to break like Python3 did.

Clojure is also a hosted language by design, and has been ported to JS, .NET, and BEAM (though maybe not completely on that one). If JVM2 were to come out, and if it supported garbage collection, it’s likely that porting it will be easy enough for someone to handle the task of transitioning the JVM1 build to JVM2. Implementing a lisp is not a rare hobby, and IMO Clojure’s language design makes it a particularly tasty flavor of lisp.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: