Applying Functional Programming to a Complex Domain: A Practical Game Engine PoC
Posted by IngloriousCoderz@reddit | programming | View on Reddit | 79 comments
Hey r/programming,
As a front-end developer with a background in the JavaScript, React, and Redux ecosystem, I've always been intrigued by the idea of applying FP to a complex, real-world domain. Even though JavaScript is a multi-paradigm language, I've been leveraging its functional features to build a game engine as a side project, and I'm happy with the results so far so I wanted to share them with the community and gather some feedback.
What I've found is that FP's core principles make it surprisingly straightforward to implement the architectural features that modern, high-performance game engines rely on.
The Perks I Found
I was able to naturally implement these core architectural features with FP:
- Data-Oriented Programming: My entire game state is a single, immutable JavaScript object. This gives me a "single source of truth," which is a perfect fit for the data-oriented design paradigm.
- Entity-Component-System Architecture: Each entity is a plain data object, and its behavior is defined by composing pure functions. This feels incredibly natural and avoids the boilerplate of classes.
- Composition Over Inheritance: My engine uses a decorator pattern to compose behaviors on the fly, which is far more flexible than relying on rigid class hierarchies.
And all of this comes with the inherent benefits of functional programming:
- Predictability: The same input always produces the same output.
- Testability: Pure functions are easy to test in isolation.
- Debuggability: I can trace state changes frame-by-frame and even enable time-travel debugging.
- Networkability: Multiplayer becomes easier with simple event synchronization.
- Performance: Immutability with structural sharing enables efficient rendering and change detection.
I've created a PoC, and I'm really enjoying the process. Here is the link to my GitHub repo: https://github.com/IngloriousCoderz/inglorious-engine. You can also find the documentation here: https://inglorious-engine.vercel.app/.
So, when and where will my PoC hit a wall and tell me: "You were wrong all along, FP is not the way for game engines"?
NarrowBat4405@reddit
When you actually create a proper videogame thats not a toy project, with thousands of lines of code.
FP is just a more restrictive way to express stuff. Thats all. And videogames are indeed one of the software fields that in my opinion don’t play well with this. Most videogames are “glorified” simulations, and simulations are so much better expressed with… just actual classes, inheritance and mutable state. (And composition over inheritance has nothing to do with FP, thats accomplishable with OOP aswell)
IngloriousCoderz@reddit (OP)
Hey thanks, I appreciate the feedback! Though I have to respectfully disagree with some of your points, especially the assumption that this is just a toy project.
You are correct that a game with thousands of lines of code is a complex beast. As an OOP codebase grows, managing a massive, interconnected state becomes one of the biggest challenges. You get into a situation where a bug in one object can unexpectedly affect another, leading to "spooky action at a distance" that is incredibly difficult to track down.
I believe that FP is not a restrictive way to express things; it's a more disciplined way. That discipline is precisely what makes it an ideal solution for managing complexity at scale.
You are also correct that composition is not unique to FP. It's a general software principle. The difference is that while it is an option in OOP, it is a foundational and idiomatic principle in functional programming. My engine shows how this principle is a natural fit for building a scalable simulation without the brittle inheritance hierarchies that can plague large OOP projects.
Ultimately, both paradigms are powerful ways to model the world. I'm exploring an alternative that I believe offers a better approach to managing complexity at scale, and this PoC is my first step in proving that.
NarrowBat4405@reddit
I didn’t said that THIS is a toy project. I said that you will discover that applying pure FP into mainstream videogame software is not a good idea when you do a non toy VIDEOGAME PROJECT, using your engine.
The thing is that I think FP IS actually a restrictive way to express software solutions. OOP indeed lets you apply any FP idea, it is a more broad paradigm, but the inverse is not true. Pure FP wants you to completely eliminate mutable state. And from my opinion thats a terrible idea when writing videogame software as I said in my reply before.
Videogames often have THOUSANDS of mutable states of individual objects, if not millions. The sole position vector of each object is a mutable state. There’s no way you can express that better with FP instead of OOP. So yes you’re gaining “explciit data flow and predictability and testing” at the cost of… expressing simple state stuff with restrictions you imposed yourself by your paradigm. You’re paying a real cognitive cost.
Thanks for your respectful response. I actually enjoy a lot discussing the “FP vs OOP” (at overall, not just in videogame software development) classic discussion because I also believed the whole “FP is the definite paradigm” thing.
tdammers@reddit
It kind of is though.
You can represent an object (in the OOP sense) in an FP language as a record of functions that take a reference to the object as their first argument.
The reason people don't do this is twofold:
this
pointer), or a simple module system. If you can think of a good example, I'll be all ears though.Given that, I think it's no coincidence that the Haskell ecosystem doesn't have a convincing GUI story - that's the one thing where OOP systems shine, and the benefits of a language like Haskell become liabilities - you're doing the same thing that those OOP languages do, only worse and with uglier syntax.
But, again, it's not like FP is a subset of OOP, nor the other way around - both are Turing complete, and you can express either of them in terms of the other.
NarrowBat4405@reddit
I didnt said that FP and/or OOP weren’t turing complete. I didn’t said that FP is a subset of OOP.
OOP does not impose any restriction on how you express solutions. It just give you more tools: encapsulation, inheritance etc etc. FP while giving you some tools, it also says: “mutable state is bad. You should avoid it whenever possible” This does not mean you cannot express something using pure FP. As you said, FP is turing complete. The problem is that while you can write any solution sticking to it, there are many cases where that solution is unreadable mess that even might rely on duplicated code. Thats a fact.
Of course the same happen if you go the other way around: most of “design patterns” are just verbose and crappy ways to achieve the same you get with first class functions.
But the thing is: everyone is fine by doing OOP + FP. Thats literally how all the modern programming looks like. Restricting yourself and your team to pure FP is generally a bad idea unless you have a very specific and justified reason to do so
tdammers@reddit
But it does. If you do OOP by the book, then there will be no "free functions" (that is, behaviors other than methods), and no "unadultered data" (that is, data that can be accessed or manipulated directly outside of the context of the owning object, nor data that isn't associated with any object at all).
Most OOP languages have "backdoors" to allow these things in some form or other, openly or covertly, but the same is true of "FP languages", practically all of which have "backdoors" to allow for mutable state and side effects.
In other words, the languages do not impose any restrictions, for entirely pragmatic reasons, but the paradigms do.
Encapsulation isn't unique to OOP. Practically all typed languages, and most untyped ones, regardless of paradigm, provide a way of hiding implementation internals from the public API at some level. FP languages typically support closures, practically every language with a module system allows you to selectively export things from a module, and even good old C supports hiding implementation internals by not exporting them through the header (you can also declare procedures as "static", which makes them inaccessible from outside the compilation unit). Encapsulation is not an "object" concern, it's a "module" concern - it's just that in most OOP languages, objects also take on the role of modules, for most practical intents and purposes, and in languages that do have a separate "module" concept, that concept is usually rudimentary, redundant, or tightly coupled into the object system. At the same time, the OOP paradigm doesn't actually hinge on encapsulation - an object system that cannot enforce visibility will still work, and it will still allow you to fully implement OOP semantics (e.g., Python's OOP system doesn't really enforce encapsulation, it just strongly encourages it by hiding members whose names start with an underscore by default - they are still accessible, but because Python is untyped, and any programming discipline is essentially an honor system, it still works, and you can still write textbook OOP code in it).
Inheritance isn't unique to OOP either; the concept of "this thing is like that thing, except X, Y and Z" is readily available in procedural and functional programming too. A function can inherit from another function simply by selectively deferring to it; so can a procedure. Records, available in both procedural and functional programming, can trivially inherit from other records - just copy the original record and overwrite some fields. Some languages also support "extensible records" natively, that is, you can define record types in terms of other record types ("this record type has the same fields as that record type, but also these fields"), and again you can put functions or procedures into records as well, so this can also be used to implement polymorphic records-of-functions.
The key thing about OOP, IMO, is "open recursion", the ability to have methods call back into other methods of the owning object, and have those calls be resolved according to the object present at runtime. That is, if you have an object
A
with methodsfoo
andbar
, andfoo
callsthis.bar
, and then you have an objectB
that inherits fromA
and overridesbar
, then callingB.foo
will callB.bar
, notA.bar
, even thoughB.foo
is the same asA.foo
, andA
knows nothing about the existence ofB
orB.bar
.(Side note: there is another definition of OOP, "Kay style OOP", which can be summarized as "whatever it is that Smalltalk does"; modern OOP is quite different from that, and one could argue that Erlang's actor model is maybe the closest to that in modern programming languages - but that's not what people generally mean when they talk about "OOP" these days).
Anyway, the thing with open recursion is that it's both a tool and a restriction - OOP gives you the ability to use open recursion, but it also forces you to deal with it at all times, because any object you work with may be a descendant of the type you demanded, and any methods you call on it may resolve to whatever the descendant wants. Open recursion gives you a lot of flexibility in what you provide, but it also takes away a lot of certainty in figuring out what you'll receive.
The same is true of the "restrictions" FP gives you. You gain the ability to mark expressions as "pure", and that allows you to completely ignore effects when reasoning about them or refactoring them, but of course it also means that you can't use anything that requires side effects (including mutable state) in a pure context.
Bit of a fallacy here - first of all, they're not mutually exclusive, nor are they the only choices - procedural programming is still available, in fact most mainstream languages (whether they label themselves "object oriented" or "functional" or "multi-paradigm") trivially support it (though some of the more zealous ones require a bit of ceremony to pay lip service to their declared paradigm - e.g., in Haskell, if you want to write procedural imperative code, you have to use the
IO
type and monadicdo
notation; in Java, you have to declare pro-forma classes so your procedures can be "static methods"); and you can implement OOP semantics on top of an FP language, and you can write pure functional code in an OOP language. It may not be the most convenient approach, nor idiomatic, but the choice of programming paradigm is only loosely tied to the programming language you use, and you can certainly mix and match styles as you see fit.Well, yes, obviously - a pure FP program cannot actually do anything except heat up the CPU, because pure code cannot be effectful, so there is no way a pure program can interact with the rest of the world (look up "Haskell is useless" on YT, where Simon Peyton-Jones, the "father of Haskell", explains this issue).
A common design here is to separate your program into a "functional core" and an "imperative shell" - that is, the "shell" handles inputs, outputs, and whatever other effects you need, in an imperative fashion (you can use OOP here, but it's rarely necessary - plain old procedural code works fine), and calls into a pure function that acts as an entry point to the pure functional program logic. Something like this (pseudocode):
A design like this is not a bad idea in general; it works really well for a lot of programs actually, especially those where the main complexity lies in the program logic, and the input/output layer can be relatively simple, such as server-side web applications, compilers, data wrangling tools, spam filters, DSP programs, etc. The huge advantage of this is that practically all the complexity lives in pure code, which means it's easy to refactor, easy to reason about, and easy to test; you still have a bit of imperative code that's harder to deal with, but you can keep it small and simple, and ideally, you won't need to touch it often.
It's probably somewhat less useful for things where the input/output part is intrinsically complex, such as video games or GUI frontends - the "imperative shell" is simply going to be very large, and so the benefits of keeping the logic in a separate pure functional core are relatively small. Input/output concerns are also more tightly coupled to program logic - for example, in a video game, adding an entity to the scene requires loading the relevant assets (3D model, textures, sound effects, etc.) from disk, adding them to the scene graph, and rendering them on screen; with the pure core / imperative shell approach, the imperative shell will now have to detect that the object has been added to the program state, and infer which assets to load in response, whereas if the logic is imperative and tightly coupled to the rendering, the logic itself can call into the rendering code to ask for the assets to be loaded, and store references to those assets in the logical object itself. (Note, btw., that I said "imperative" here - you don't actually have to use OOP for this, it can also be done procedurally, or using some sort of in-memory database approach, which you might call a "relational programming paradigm").
NarrowBat4405@reddit
You’re totally correct, but please be pragmatic here. When we speak that “OOP is encapsulation, mutable state packed with logic, inheritance, polymorphism etc” we’re referring that the language has direct constructions to easily implement all of this (proper classes). Doing weird emulations like you do in Go and C aren’t an excuse to say “hey but those languages DO support OOP!” They doesn’t.
And you’re totally missing my point. What I’m saying is that sticking to one paradigm (and all of its questionable restrictive rules like no declaring stuff freely (OOP as you said) or inmutability (FP) is the nonsense that will lead to have an unmaintainable mess long term, for non toy projects of many LoC.
tdammers@reddit
But then if you reverse the logic, taking a typical OOP language, writing some immutable data structures and stateless behavior objects in it and saying "hey, but this language DOES support FP" is just as silly.
I don't think I'm missing your point.
Yes, religiously applying a paradigm to its fullest even when it's clear that it's no longer pulling its weight makes for messy code; there's no doubt about that.
But I do feel that there is a fundamental difference between OOP and FP here.
Pure code (including immutability) generally makes code easier to read, understand, test, refactor, and work with; I have yet to see code that can be written in a pure fashion but became messy because of it. I suspect that 99% of the time, the case is either that the code can be pure, and writing it as pure code is hands down beneficial (except, maybe, on the performance front), or it simply cannot be written as pure code at all (because it needs to interact with the outside world, i.e., it needs effects). This means that when working from a pure FP starting point, it's usually pretty obvious when and where you need to abandon purity - whenever you need to interact with the outside world, or whenever it is clear that there exists a solution involving mutable state that is significantly more performant than the pure solution.
With OOP, it's not that obvious - if your starting point is to express everything as objects (i.e., mutable state hidden away behind behaviors tightly coupled to that state, in a runtime-polymorphic fashion), then that's an approach that can be used pretty much 100% of the time, but it's not always the best approach. Yes, you can tap into the FP wisdom of avoiding shared mutable state and uncontrolled side effects, writing classes that represent either immutable data, or stateless behaviors, but it's not the default, and it's rarely clear whether you should be doing this or not.
Oh, and: those "questionable restrictive rules" aren't really that - they're the core of how you decide to think about programs, "unifying ideas". These unifying ideas are mainly about what we want to use as the fundamental building blocks for our programs. For imperative code, the fundamental building block is the "statement" (an instruction telling the computer what to do next); for OOP code, it's an "object" (an entity consisting of hidden internal state and associated behaviors, exposed through a public interface); for FP code, it's a "function" (in the sense of a pure mapping of input values to output values). Choosing a programming paradigm is not about the restrictions you impose on yourself, it's about the fundamental building blocks you want to use.
And in light of that, I'd like to challenge the idea that sticking with a single paradigm is a recipe for making a mess - on the contrary, mixing different kinds of building blocks in an ad-hoc fashion will make things messy, and you'll easily get into a "worst of both worlds" situation, where you cannot capitalize on either paradigm's strength. If you do have a large project for which neither paradigm is suitable in its pure form, then you will have to deal with that; but IMO it's better to make that a conscious choice, and to have clear boundaries between those paradigms - whether those are along feature boundaries, layers, abstraction levels, etc., matters less than that they exist.
E.g., you might want an imperative rendering and input engine driving a functional logic core - that's fine, because it means you can capitalize on the strengths of FP (easy reasoning due to absence of side effects) within the parts that are written in that paradigm, and leverage the strengths of imperative coding (being close to how the computer works, making it easier to write efficient code and controlling the details of execution) in the parts written imperatively. But throw some uncontrolled side effects into the FP part, and the whole advantage goes out the window, because now you can no longer be sure that the code you're looking at is in fact pure, and you cannot safely apply equational reasoning anymore.
mascotbeaver104@reddit
I don't think you know what OOP is lol.
Inheritance, encapsulation, or even polymorphism are not "features" of OOP. Plenty of procedural/FP languages have that. The unique "thing" with OOP is tying logic and mutable state togething in a single bundle and, in theory, preventing objects from directly mutating one anothers states. If you aren't doing that, you aren't really doing OOP, you're just writing procedural code, perhaps in an OOP framework (if you're using Java or C# and are literally forced to represent your program as a class).
IngloriousCoderz@reddit (OP)
OMG you are absolutely right, you never said that my game engine was a toy project! My sincere apologies for the misunderstanding; I completely misread your initial comment. Thank you for taking the time to clarify your position.
I couldn't agree with you more on the final point about the blend of paradigms. I don't believe FP is the definitive paradigm either. In fact, for things that need frequent, volatile updates like bullets, I would absolutely use object pooling and mutability. The key is knowing which tool is best for the job.
Where I think FP truly shines is in managing the predictable state of the game world. You mentioned positions, and that's a perfect example.
In a large game, the biggest performance bottleneck isn't usually the state change itself; it's detecting which objects have changed and need to be rendered.
In a fully mutable system, if you have a thousand objects, you would have to check the position vector of every single object, comparing each of the three numbers to see if it moved. This is a very expensive, deep comparison.
With immutability and structural sharing, the approach is different and often more performant. For every position that changes, a new array of three numbers is created. For every position that doesn't change, the reference to the old array is kept.
To decide if an object needs to be re-rendered, you just perform a simple reference check. Is the position a new array or a reference to the old one? This is a single, lightning-fast comparison, far more performant than checking every single number in every single vector.
So while you're right that a new array is allocated, you're paying a small memory cost for a huge performance gain in change detection, which is often the bigger bottleneck in a game loop. You're trading a little bit of allocation overhead for a massive boost in rendering efficiency.
jleme@reddit
It's actually a well-known and widely used optimization to avoid brute-force "dumb" updates. You don’t need to reprocess everything, only what changed.
Here, though, you’re using an FP feature as a change-tracking flag. FP itself isn’t what is improving performance, it’s the pattern. And that same pattern works just as well in OO. Honestly, it also worries me how tightly array allocation is coupled to the rendering logic.
tdammers@reddit
It's not like you can't do this in a mutable-state system though, you just need to flag objects as "dirty" when you update them, and reset the "dirty" flag when you've rendered them.
And the real killer here is the iteration itself, not the check - what you really want to do is collect the changed objects into a smaller dataset as you go, and then iterate over that. In pseudocode:
FP does shine, however, in staying on top of where your state lives; OOP tends to scatter program state all over the place, making things like savegames, replays, etc., incredibly tricky, whereas OP's FP design makes them absolutely trivial (all you need to do is take your top-level game state and serialize it; that's it). It also shines at separating high-level concerns: everything except the low-level input and rendering routines can be implemented in pure code, that is, no side effects, just functions taking state and returning updated state. You can trivially mock out physical inputs, the rendering pipeline, the file system, network connections, and even time itself - all these things are just immutable data going into your state update and coming out of it, and the game logic doesn't care where that data comes from or where it's going to.
devraj7@reddit
And this is exactly why FP can't scale: creation of objects, i.e. memory allocation, is what kills performance.
On a separate note, your message gives me strong vibes of being AI generated, especially (e.g. "You are absolutely right").
IngloriousCoderz@reddit (OP)
I can assure you that my messages are 99% hand-written, with some occasional proof-read by an AI just because I'm not sure of my English ^_^" I'll write this one totally by myself for you.
Creating a new 3D array at every position change is a waste of CPU, but gives you advantages that compensate by a long shot the drawbacks. The process is actually very simple, and very similar to how software like Git work under the hood:
That seems a huge waste of space: making a copy of a whole file even if I changed one line? Isn't it better to store a diff? Well no, because by wasting a little bit of space (we are talking about text files after all) you gain superb performance in time. To know the state of your repo at every commit you just look at its files (those that changed) and the references to old files (those that did not change). With diffs you would have to apply those diffs all along the commit history.
Immutability in FP works pretty much the same way: positions that didn't change are kept as references, positions that did change are created anew. How do I know if a position changed? The reference I have points to a new object. It's a matter of reference equality, a === b. It's super fast.
devraj7@reddit
Copy on write is super fast only if you don't have mutability.
You are still ignoring the fact that there are a lot of fields, such as games or neural networks, that require millions of mutations every second. Allocating memory for each mutation simply doesn't scale.
IngloriousCoderz@reddit (OP)
Yes, that's a very common and important concern. It seems counterintuitive, but for millions of small changes, this approach can still be more performant overall.
The core of the argument is a classic engineering trade-off: memory allocation vs. CPU cycles.
In a traditional mutable system, when you update a property (like an object's position), you are modifying memory in place. This is very fast. The problem, however, comes when you need to know which objects have changed to, for example, render them on the screen. To do this, you would have to check every single object and compare its properties—an expensive, CPU-intensive process.
In my engine's immutable approach, you are paying a small price in memory allocation upfront to save a huge amount of CPU time later.
With millions of changes, this trade-off becomes even more apparent. My engine doesn't have to perform a massive, element-by-element comparison to find what changed. It simply checks if a reference is new.
This is a well-known pattern in many high-performance systems. For example, the software that handles video game assets uses a similar pattern. When a game's state updates, only the changed files are updated on the disk, and the rest are simply referenced.
Modern JavaScript engines are also highly optimized for this kind of workload. They are very good at efficiently allocating and garbage collecting millions of small, short-lived objects without causing major performance issues.
By making this trade-off, you're not just gaining speed; you're also gaining predictability at scale. You are solving the biggest problem in a complex system: managing state and change in a way that is traceable and debuggable.
NarrowBat4405@reddit
You’re right. Maybe as an engine foundation, using pure FP could be a very good idea actually. But is a good idea to fully stick to the FP paradigm only for the engine itself? Maybe you can tell based on your experience on this project. I still believe it’s just better to do OOP + FP for any large scale project, but this kind of project might reflect a less necessity of using OOP concepts. Personally even for large scale backend REST API software (which naturally fits perfectly with the FP paradigm) I needed to do any of encapsulation, mutable state or inheritance. (Without ending up duplicating code or writing unmaintanable code). The performance example you provided is perfect, but nothing prevents you to just implement this in a pure FP way or even OOP + FP while keeping the performance gains. My concern is to applying pure FP everywhere where it does not make sense.
For commercial videogame projects though I still believe that there’s no way you go full FP exclusive without messing the source code by creating something that only FP guys understand.
IngloriousCoderz@reddit (OP)
Totally agree, that's why I invite you to have a look at the game examples here: https://inglorious-engine.vercel.app
I tried as much as possible to make FP not get in the way of the Developer Experience, in fact I believe it looks almost OOP. Please have a taste and let me know what you think!
Ameisen@reddit
/təˈmeɪ.toʊ/, /təˈmɑː.təʊ/
IngloriousCoderz@reddit (OP)
If you're thinking of FP as a dogma, yes. OOP can also be restrictive if you don't allow yourself to use a lambda from time to time.
As for me, if you read the docs you will see that I took from FP what suited my purposes, restricting where I wanted to restrict, but also bending the rules of FP where I needed (e.g. event handlers are not really pure functions).
As some other commenters already said, neither OOP or procedural or FP are the way, but probably a mix of all of them. My PoC has a different twist in that it doesn't start OOP and adds some FP, but it starts FP and adds some non-FP for convenience.
Ameisen@reddit
There's a reason that I use C++: it doesn't lock me into any specific paradigm.
IngloriousCoderz@reddit (OP)
And for the same reason I prefer JavaScript! Glad to hear that we have pretty much the same goal, despite achieving it with different means!
teerre@reddit
That's complete nonsense. Even discounting that a lot of game developers today will tell you that the OOP koolaid is terrible (clean code, terrible performance, remember?), the only reason you think that is because the gaming toolchain has been using OOP for a very long time, thousands and thousands of engineers-hours were spend to improve the workflow. It's obvious that it will feel "better" for someone judging it superficially
NarrowBat4405@reddit
Nope. There’s a reason OOP is still the dominant paradigm. There’s a reason typescript has full support for OOP. And performance is terrible in FP, not OOP. If you don’t know that you have no idea what FP is.
jleme@reddit
This pseudo-logic, that dominance equals "best", doesn't hold up. Performance used to be terrible in OO as well.
NarrowBat4405@reddit
Yeah you said it, used to be fortunately. In FP you’re doomed from the inefficiencies of creating new objects every single time.
jleme@reddit
If it’s not obvious, let me clarify. What used to be one way in the past might not stay that way in the future. Today’s inefficiencies could turn into tomorrow’s "used to be’s".
NarrowBat4405@reddit
I see. Yeah I would take your word… if pure FP offered HUGE benefits, but it does not.
Even if FP were hyper-performant, It makes your code a lot harder to read. So today you’ll end up with less readable code AND less performant. Sure, less bugs, but it is worth the cost? No.
Not at least for non toy projects.
jleme@reddit
"Even if FP were hyper-performant, It makes your code a lot harder to read" OMG!
So you do think FP makes code harder to read. Okay, now I get it.
LOL 😆
Nvm...
NarrowBat4405@reddit
It is, me and many other programmers believes that. If not the majority of programmers. All that deep crap terminology that no one but FP cultists understand make code harder to read. Code that only “geniuses” understand is paradoxically not genius at all.
Oh and “understanding” FP makes you feel intelectual? Yeah sure, whatever you say if makes you feel better 👍
IngloriousCoderz@reddit (OP)
I couldn't agree more in general but, have you checked my PoC? Please have a look at the code examples and tell me if they are unreadable as the usual FP: https://inglorious-engine.vercel.app
I'm sorry that my post was intended by some as bashing on OOP in favour of FP, but it's nothing like that. I didn't even mention OOP once in the post.
NarrowBat4405@reddit
The code looks like standard javascript to me. Unreadable code comes more from stuff like Haskell or Elixir, which enforces you to doing pure FP in the language itself (which will inevitably lead to unreadable code harder to maintain in my opinion, long term).
You have nothing to apology. Your post naturally attracted many pure FP cultists that I just replied to.
jleme@reddit
Yep, you’re totally right. Feeling better? Good. Don’t forget your milk and head to bed early, okay?
Maybe-monad@reddit
Big inheritance chains will result in worse performance than doing FP
NarrowBat4405@reddit
Creating new objects every single time results in worse performance. Thats a fact, and pure FP even forces you to do so. Big inheritance chains maybe, but OOP never forces you into doing that. You can even use OOP principles without even using inheritance.
See the difference?
Maybe-monad@reddit
Without data to back it that is a claim born of misconception not a fact. If you create many objects doesn't mean they'll exist when your program runs, Rust iterators are the most obvious example. There's also the fact that pure FP doesn't restrict mutation, in Haskell you can use the state monad if you're worried about GC pressure, the trick used by Haskellers to implement sorting algorithms with C-like performance.
NarrowBat4405@reddit
Inmutability, which is a core principle in pure FP, IS slow by design. It is literally contrary on how computers internally works. You don’t even need “backup data” to realize that that claim is the truth.
But in any case, using google for two seconds will still reveal how there is an endless list of discussions regarding how slow is pure FP. Here you have some of them:
https://news.ycombinator.com/item?id=7624740
https://discourse.julialang.org/t/functional-programming-is-not-capable-of-achieving-absolute-top-performance/119233
Also pure FP ENFORCES inmutability. It IS a core principle (Elixir literally does not allow mutability at all). Wether you use weird “tricks” to emulate mutability (which actually hints how mutability is essential) is totally irrelevant to this conversation.
I’m so sorry to break your illusion, my dear FP cultist (inferred of course from your name, “Maybe-monad”), but this is the hard truth you can’t see because of your cognitive dissonance. Don’t worry, that’s a common behaviour pattern among cultists of any topic.
Maybe-monad@reddit
Before lashing out with ad hominem attacks, which is, in my opinion, a sign of ignorance and immaturity, you should use your friend Google to learn how monads can be used to perform side effects with preservation of functional purity.
NarrowBat4405@reddit
I provided counter-arguments to what you said. I didn’t even insulted you, I said you’re a cultist (which you clearly are). Wether that offends you or not is not my problem. I didn’t used that fact to backup my argument so it’s not a fallacy.
I don’t care about weird tricks you mention because I’m not a pure FP cultist. Even while I understand many of the principles of pure FP and even apply many of them on practice, I do not practice doing full real life non toy projects in pure FP while restricting me into doing other paradigms.
You said that pure FP does not restrict mutation. That’s a lie no matter what tricks you use. Pure FP do restrict mutation.
Maybe-monad@reddit
You can find the proof for how the state monad preserve functional purity in the following article: https://iris-project.org/pdfs/2022-oopsla-purity-of-ST.pdf.
Regarding your being a cultist you fit better into the role because of how you act when your beliefs are challenged.
NarrowBat4405@reddit
We’re not discussing how your trick presserves purity. That’s only relevant to pure FP cultists. FP RESTRICT MUTATION.
How hard it is to understand? What you’re saying is like saying that old nearly “pure” OOP languages like Java supported first class functions by doing the Command pattern. That’s not first class functions!!
I’m going to start ignoring you because talking to you is like talking to a wall. Typical from cultists.
eambertide@reddit
I actually mostly agree, and actually upvoted!, you, however I will not that actual, good functional programming languages like Clojure implement optimizations such as tail call recursion optimization and lazy container operations which significantly speed up their languages
NarrowBat4405@reddit
Correct, actually how the language compiler/interpreter implements object recreations is (or should be) irrelevant to the programmer and should have only an impact on performance. But my point is that going purist on FP is as pointless as going OOP without first class functions. Restricting yourself from which coding tools you can use when there is no clear justification makes no sense and harms the project long term.
eambertide@reddit
Oh no definitely! Just wanted to leave a note for other readers as I find Javascript (and its third party dependencies)’s handling of immutability and functional programming extremely annoying since they seem to make none of the performance optimizations necessary to make it work and still sell it as a holy grail
Blue_Moon_Lake@reddit
Except TypeScript doesn't have full support of OOP. It can't have
instanceof
right due to duck-typing classes with only public members.Which is unusual, but it happened to me a few times with abstract classes that have only public abstract members defined.
NarrowBat4405@reddit
It has full OOP support. This is a particular quirk due to typescript implementing OOP on compilation time rather than runtime (caused by structural typing instead of nominal typing). But it does not mean that it does not support all the basic and fundamentals of OOP.
Blue_Moon_Lake@reddit
Duck-typing classes with only public members is a choice of turning the class type into a generic object type, not a "quirk" of OOP implementation.
NarrowBat4405@reddit
Thats just how structural typing works and one of its consequences. It does not invalidate the fact that it has full OOP support. Languages that does not are C and Go, not typescript. Whether you like it or not is irrelevant.
teerre@reddit
It's ok, hopefully in your career (supposing you're an actual programmer) you'll eventually learn a bit more of computer science and understand how things really work
NarrowBat4405@reddit
I already do, it seems that you’re the one that don’t. You’re unable to explain and refute anything
Determinant@reddit
While functional programming has many benefits, game engines are a poor fit due to the memory and performance impacts. For example, mutable objects improve CPU cache hit rates since they remain in the same place in memory instead of allocating a new object for each change. Mutation also reduces memory allocations and pressure on the garbage collector etc.
I'm sure it will be fine for toy examples but it won't scale once you have thousands of entities interacting with the world in a non-trivial manner.
IngloriousCoderz@reddit (OP)
Hey thanks for the feedback! I appreciate you bringing up these points. You're completely right that these are critical considerations for any game engine, and they represent the core technical trade-offs that have to be made.
You're correct that mutable objects can benefit from better CPU cache hit rates. However, my engine's data-oriented architecture also addresses this. By organizing the game state into a single, cohesive data object, it allows for more predictable memory access patterns. This can be more efficient than the scattered memory access that often happens with a traditional class-based, object-oriented approach.
As for memory allocations and garbage collection, my engine chooses a different set of trade-offs. It pays a small memory allocation cost to gain a massive advantage in CPU cycles by allowing for lightning-fast change detection. In many scenarios, this should be a net performance win.
The biggest challenge in building a complex game with thousands of entities isn't just raw performance; it's managing complexity. A highly mutable system with thousands of interacting objects leads to unpredictable bugs that are incredibly difficult to track down. My engine's core philosophy—with its explicit data flow and single source of truth—is specifically designed to solve that problem. It makes the entire system predictable and easy to reason about, which is what truly allows a project to scale.
Determinant@reddit
I built a game engine before.
Game engines should definitely use a component entity system with data oriented design for best performance. I'm not advocating for traditional object-oriented design.
What I'm saying is that using these techniques with a functional immutable approach introduced many large negative performance impacts.
While a single cohesive data object might simplify the mental model for you, CPUs don't care about high-level concepts so duplicating this object every time the world changes is bad for performance as there is nothing predictable about this approach from the CPU perspective (branch predictor, memory prefetch, L1 / L2 caches, etc.).
A mutable data-oriented approach will be significantly faster than a functional immutable approach and complexity can be kept low with a proper architecture.
IngloriousCoderz@reddit (OP)
Thanks for the clarification. It's valuable to know your perspective comes from direct experience. I have to respectfully disagree though with your conclusion that a functional, immutable approach introduces large negative performance impacts, especially in the context of modern JavaScript.
You're right that a mutable, data-oriented approach is generally considered the fastest option in low-level languages like C++. The performance benefits you mentioned, like CPU cache hits and avoiding memory allocation, are absolutely critical there.
However, JavaScript's managed runtime fundamentally changes the rules of the game. My engine is built on the assumption that modern JavaScript engines are so advanced they make the performance trade-offs of functional immutability viable.
When you write JavaScript, you're not writing code that directly runs on the CPU. The engine's Just-In-Time (JIT) compiler and garbage collector perform a huge amount of optimization to make your code fast.
data.x
,data.y
access is highly optimized, even with new objects.My engine is an exploration of whether these engine-level optimizations are mature enough to make a functional data-oriented design a viable and even superior alternative to traditional mutable designs.
Boxfort_@reddit
Ai generated slop
jleme@reddit
I’m getting really tired of people acting like geniuses just because they can shout "AI slop". Like, congrats! You spotted a polished sentence and think that makes you a human lie detector. Big difference between someone using AI to clean up their writing and someone parroting half-baked AI ideas without knowing the subject.
This whole "AI slop" thing is played out and honestly exhausting.
The guy’s making technical arguments. If you actually understand them and want to push back, go for it. If not, maybe just don’t. Tossing out "AI slop" as your only response is lazy and annoying.
juhotuho10@reddit
Bro, it's like straight up copied from a chat windows, it's pretty easy to recognize from wording
IngloriousCoderz@reddit (OP)
I'm getting this criticism quite a lot, and I understand. I'm sorry if my wording feels robotic, but sometimes I use AI to proof-read my words since English is not my first language. So, to be clear: the concepts are all mine, they way I express them can seem auto-generated.
BTW, here's how Gemini revised my current comment:
"I appreciate you bringing that up. I've heard that a few times, and I completely understand the feedback. Since English isn't my first language, I often use an AI to help me proofread and refine my writing. The core ideas and concepts are entirely my own, but the phrasing can sometimes come across as a bit polished and impersonal."
jleme@reddit
Yeah, I get that. I’m not saying it wasn’t made by AI. But is it actually slop? Show me.
Again: The guy’s making technical arguments. If you actually understand them and want to push back, go for it. If not, maybe just don’t. Tossing out "AI slop" as your only response is lazy and annoying.
Gosh, people love to rush in with empty arguments without using even 1% of their brain. Everyone’s scrambling to be the first to yell “AI slop” just to show off how clever they are.
Ameisen@reddit
It isn't polished. The issue is that he's using a lot of words to say very little, and much of it is redundant or repetitive. That's how ChatGPT tends to write.
There's also certain patterns that are in each of the comments which are also common in generated text.
jleme@reddit
It would be really helpful if you could highlight parts of his text where:
- he uses a lot of words to say very little
- the writing is redundant
- the writing is repetitive
Ameisen@reddit
Why would that be helpful? Who or what would I be helping if I were to do that?
jleme@reddit
So yeah, thanks for proving my point.
"AI slop" wasn’t analysis, it was just a cheap buzzword to sound clever. And when it came time to back it up, there was nothing behind the curtain.
Case closed.
IngloriousCoderz@reddit (OP)
Oh come on, don't be too harsh on yourself!
Determinant@reddit
My expertise is on the JVM where those types of optimizations are even better than JavaScript engines like V8.
There's lots of knowledge that has been lost in translation resulting in misconceptions such as what you're saying. Interpreted languages like Java used to be 50X slower than C++ so these types of optimizations that you're mentioning have made huge leaps for bringing that closer to parity.
For example, the common knowledge that short-lived objects are cheap is true in the general sense. However, I did a quick test where I replaced short-lived objects with the frowned-upon old practice of object pools and my game engine got a free 30% performance boost on a recent JVM.
Unfortunately you're repeating tribal knowledge that typically applies in other domains such as UI or backend where network calls dramatically dominate any of these impacts making the optimizations useless. Most of this knowledge doesn't apply to latency sensitive domains like game engines.
Ameisen@reddit
You're responding to a chatbot.
IngloriousCoderz@reddit (OP)
Thank you for this detailed and very insightful response. I genuinely appreciate you sharing your expertise from the JVM, and I have a lot of respect for your experience.
I agree with your point about object pooling and the cost of short-lived objects. You're right that even with advanced GCs, there is still a significant cost to memory allocation. This is precisely why I'm not pursuing FP nirvana. I'm not a purist. For very volatile, high-frequency objects like bullets, I was already planning to use a more mutable approach to optimize for performance.
You’ve rightly pointed out that my approach has trade-offs. I'm choosing to accept the costs of a managed runtime to gain simplicity and predictability. For example, my event handlers are not pure functions; they have side effects like notifying of other events.
My project isn't an attempt to prove that a functional JS engine can compete with a highly optimized C++ engine on raw performance. It's an exploration of whether a pragmatic, hybrid architecture can offer a better balance between developer experience, debuggability, and a new kind of performance that's valuable for my specific domain.
Ameisen@reddit
This reads like ChatGPT...
IngloriousCoderz@reddit (OP)
It does read like ChatGPT, sorry for that. Since I'm not a native Englsh speaker, sometimes I express my thoughts, they don't sound well, so I ask an LLM to express them better. The result feels a bit robotic. I'm going to rephrase the previous comment in my own words:
edparadox@reddit
Is there a reason you're speaking like an LLM?
IngloriousCoderz@reddit (OP)
Actually yes! Sometimes I express my thoughts in English, then see that my English is broken, then ask Gemini to rewrite them, realize that Gemini writes better than me, and finally after some adjustments I use Gemini's version 'cause I couldn't express myself better.
BTW, this was my original version. The polished one, suggested by Gemini, is:
"Actually, yes. English isn't my first language, so after I write my thoughts, I'll often use a tool to help me polish the grammar and phrasing. I find that it helps me express my ideas more clearly, and I couldn't have said it better myself."
Rattle22@reddit
Hey OP, your writeup (and comments) are very eh from a writing perspective. You underexplain interesting bits while wasting words on unhelpful little tidbits. It reads like AI in the worst way: Wordy and devoid of thought or character.
It's irrelevant to the quality of your programming work, but if you want to continue sharing your journey(s), I urge you to spend some time critically engaging with writing.
IngloriousCoderz@reddit (OP)
Hey there, thanks for the feedback! I'm sorry if I write like AI. I have to admit that sometimes I make AI proof-read my writing because English is not my first language, but the content is all mine. Beep-boop.
Could you please help me identify which writing is eh? Are you talking about the post and the comments only, or are the docs that I linked underwhelming too? Which are in your opinion the interesting bits I'm skipping and which are the unhelpful parts I'm over-explaining? That would help me very much improve my ability to convey the message.
Rattle22@reddit
Okay so, focusing on specifically the post writeup: The list of benefits is introduced as a showcase of how easy it is to implement features with FP, but then focuses on the comparison to OOP. This is in itself fine, but it lacks the depth to be valuable. It comes across as needlessly derogatory. If FP is as good as you claim, you don't need to talk down OOP to make it shine, and if you want to compare the options, it needs more substance. The list would be improved just by throwing out the comments about OOP.
The second list is what makes this seem so much like AI-slop. The points barely fit together thematically, don't really explain anything, and read purely like an ad script. "Networkability" for example: What is Event Synchronization? How does it help? Why is it inherent to FP?
That's what I mean by 'devoid of substance', I didn't learn anything, I just got buzzwords thrown at me. If I knew what exactly they meant, I wouldn't need to be told about them.
Now, you can make a list like that good. If each of those points linked to an article going into the topic, it'd be a helpful hub to learn about FP, and that without the burden of having to write it out yourself. If it sketched out how these benefits materialize by showing a little (pseudo-)code, even better.
I would like to contrast that to the Readme of the repository, specifically the State Management section. It provides just enough context and just enough substance to sketch out what exactly is meant, while still being concise. Do you get what I mean?
Finally, your writeup overall hints at all the cool things you learned, but we don't get to see any of them. It's too long to be a quick "hey I made this cool thing check it out :)" and too shallow to actually learn from it. It seems like the Readme answers these things, but the text doesn't manage to give the impression that it will.
To summarize: This writeup doesn't really impart any knowledge, and also doesn't really manage to point to the (at first glance really good looking!) Readme. I think it should either be a really short, highest level overview over what you did and point to the Readme (the first paragraph is decent for that), or go way more into depth to actually teach a thing about the subject matter.
IngloriousCoderz@reddit (OP)
Alright, thanks for the detailed explanation. I'll definitely try my best next time to write a more informative post, although I have a few considerations on this one:
To summarize, I feel like you and I had different expectations on my post: you wanted to learn something and maybe to defend OOP against FP's attacks. I wanted to get the expert opinion of someone who already knows the topics of game engine development and FP and can tell me what's wrong with my approach.
Iggyhopper@reddit
I could see FP being used in a subsystem of a game engine, but not for the entire game and/or engine.
IngloriousCoderz@reddit (OP)
That's a very common and pragmatic approach, and you're right, FP is an excellent fit for complex subsystems within a larger, traditional engine. Many developers use FP for things like UI, animation logic, or AI behavior trees.
However, my engine's philosophy takes that same principle and applies it to the entire game.
A Unified Approach
The reason FP works so well for a subsystem is that it excels at managing a single, complex state without side effects. It provides predictability and makes debugging easier.
My engine's core idea is to treat the entire game as a single, unified state tree that evolves over time. By applying the principles of immutability and functional composition to the whole system, I'm aiming to bring the same benefits you see in a subsystem (predictability, debuggability, and a clear data flow) to the entire game.
It's a different approach, but the goal is to solve the biggest problem in large games: the "spooky action at a distance" that comes from a highly mutable and decentralized state.
Please have a look at the docs and see if they resonate a bit more with you: https://inglorious-engine.vercel.app/
corysama@reddit
r/gameenginedevs/ would like this.
IngloriousCoderz@reddit (OP)
Thank you so much for the tip, I'll try to post there right away!