IMO, a more accurate term would be stateless programming. This paradigm is about minimizing state. Of course, as the OP mentions, state is often quite useful. But minimizing state, especially global state, particularly mutable state, and especially particularly global mutable state, is something even imperative language fans can get behind.
This line of speech usually serves to corner FP in a self-contradicting definition that can be wrestled with, and it is really aggravating.
Functional programming as a concept is well defined, and it's not merely "first-order functions". The reason why many functional languages don't have the same feature set is that programming languages are works of engineering, and that the essence of engineering is to make tradeoffs.
For instance, F#, a functional language, has many non-functional bits, because the goal pursued was to bring FP to .net, and they made sensible choice to promote interop above functional purity.
Likewise, most people doing FP with Lisp in the early days still used mutable variables, because they were limited by the machines they had.
Another example is the ability to perform all kinds of side-effects in many FP languages (Erlang, Clojure, etc.). That doesn't mean FP practicioners are totally fine with having side-effects. It means that totally separating side-effects from code in a general-purpose programming language is not always easy, and that in the interest of having useful projects, sometimes this goal is set aside. On the other hand, programs running on more limited runtimes (eg. Elm in browsers) achieve it completely.
It is really a pity that so many people can't or don't want to acknowledge the nature of engineering work, and hold imperfections against the creators, or the concepts they try to implement.
Then, sincerely, what is the definition? Does that definition succeed in excluding languages that are not considered functional, e.g. C++, Java, Python, Go? And if it doesn't, and the definition is so broad as to encompass every language, then what is the useful purpose of going out of one's way to label a language as "functional"?
This would be a great stretch if it was meant to be a reply rather than a generic statement. To me the increasing irrelevance of a term "functional" is in fact the biggest success of the FP: it is entering the realm shared by, say, the term "structural programming" which is so universal that it became meaningless. It is an amazing feat given that no other terms for programming paradigms ever have been close this much!
Given any standard about the FP, there is a definite disparity between languages that should be called functional and languages that are actually called functional. I think the increasing universality of functional paradigm explains this, and kibwen is merely proposing another term describing the latter.
It is really more about controlling state and avoiding it where deemed unnecessary, rather than adherence to a total philosophy. This is done for practical reasons (avoiding pain mostly), not for ideological ones.
I think lack of mutation is a lot more pronounced, since it has very important implications to runtime (necessary O(logn) slow down) and requires a more sophisticated type system (linear type system) in order to emulate safe mutation.
A few of pieces of the Erlang OTP infrastructure are actually just an elaborate mechanism to reintroduce global mutable state i.e. the process registry or Mnesia.
a : Int -> Int
a x = x + 2
both `:` and `=` are statements, not expressions.Are you sure about this? A total function is defined for all of its possible inputs, i.e., it must always terminate. A functional programming language with only total functions isn't Turing complete.
"Functional language" is less concrete; it's just an informal way to imply how easy or awkward it is to use these techniques in a particular language (e.g. bash is "less functional" than ML).
Literally the same thing.
That does not mean having a favorite paradigm or language is useless, of course I love Rust and I find functional programming elegant, but if you are working in a 5 years old app written in C# in a place where no one knows F#, then that should be part of your considerations.
That is a double-edged sword. On one hand it makes sense because it allows teams to be more productive. Otoh, it leads to this decade where it has become okay to create a desktop application using HTML/JS (while more performant tooling exist) simply because a lot of people happen to know HTML/JS than C#/.NET, Qt/C++ etc.
What should be just one of the factors has become the most influencing factor.
When I program I basically think in terms of abstraction and state - if the code is obviously going to be multi-threaded then I will focus on the latter. Being stateful can slow you down on one thread but fuck you hard if it's in multi-threaded environment. Abstraction should really be free if you know what you're doing (in your language if choice I suppose)
Functional programming while a valid form, is inherently better expressed by constraining the possible use cases. It's closer to electrical engineering or mechanics then "programming."
"Programming" inherently rewards extendability, interoperability and readability, therefore it rewards OOP. Now obviously computers have Both mechanical and extendable use cases so it's up in the air.
Stuff that's written better functionally tends to be optimized into oblivion very quickly, it becomes more of a hardware/math/physics problem. But you know that's fun too.
Totally agree with the article: sometimes functional is the right tool for the job, sometimes OOP.
A more recent example: parsing USB descriptors in Rust. The parser would have been trivial and easy to understand if I could have multiple mutable references to nodes, but alas I kept fighting the borrow checker.
I talked with a friend about why it was so hard, and he said "do it in a functional style". That made my borrow checker worries go away, but instead I have multiple "tree pivoters" that recursively descend through input trees while building output trees. It practically broke my brain and although it is pure, it's not nearly as readable or maintainable as one with mutation would have been.
I think OOP is taught somewhat wrong because people will dwell on contrived examples of inheritance when its real value as a paradigm is coupling state with functions. What we've learned from FP is immutability is easier to reason about, but when it isn't is when OOP shines because it gives you a sane way for managing state.
I think I ended up going with a Player API with tapes, records, CDs etc. Some could skip, others only fast-forward. I was still unhappy with it, but it was better than most examples.
I like to point folks at writing an IntelliJ plugin. I find that some of the best architected OOP programs become nothing but a collection of plugin interfaces and base classes.
But you really need to understand "is a" vs "has a", not burn your base class, let architecture emerge following the rule of 3, rather than quickly jumping on the wrong abstraction to fight duplication.
Sandi Metz really says it better than I can: https://www.youtube.com/watch?v=8bZh5LMaSmE
The downside to them is that there is that the base class in the inheritance chain (BaseModel -> Model -> YourModel) has some magic that is difficult to reason about (the upside being the other side of that same token!). Thankfully, it's reliable and there's rare occasion to dive in.
Maybe this is a me problem. I don't know how to make a nice abstraction layer like that without inheritance, so I think inheritance is sometimes the answer.
(I've deliberately and accidentally triggered bugs in pretty much every mainstream USB stack; it's a sad state of affairs, mostly because USB is practically designed to include the maximum number of implementation pitfalls... but I'm sure you know this already)
All you have to do is pass the world state to each function and return a new state.
True, yes, but...yuck. It can be clunky in a language with single-assignment. And what is this really gaining you over C?
I mean, if you stop there, sure that’s ugly. But if you model your program with that as the foundation, then break it down (and generalize where breaking it down is general), it’s pretty easy to reason about. And what it “gains you” is never having to think about something changing in a way that produces invalid or unexpected state. (This is more true in statically typed languages of course.)In any program of any real complexity, you will eventually be inclined to break the problem down into smaller pieces. If your smaller pieces are functions, you can be certain about the state that’s returned by them. If they’re stateful subroutines, then you have to think about multiple pieces at the same time.
I think it's more accurate to say that it makes you account for the possibility that an object is put into an invalid state at every mutation you do. Which can be good and can be bad. Good being that your code is safer and less likely to have errors. Bad because it's more rigid and trickier to deal with objects containing multiple errors.
That said, I still think passing the world in and out of every function is too onerous for most real world programs, and functional programming definitely takes a too-extreme stance to be pleasant.
I think a better approach is a traditional language but with a functional subsystem where you can mark functions as pure.
It’s in Javascript, so not all of it is functionally pure, but the core game logic follows that pattern. The code is heavily commented, and hopefully somewhat readable. I think the main benefit I found was that it was trivial to snapshot states and replay as needed. That made debugging easier, and might have simplified something like a save/reload feature. Although I didn’t go there for this toy project, I’d argue that testability might be another consideration.
More generally, I’ve seen over the years that global state tends to be painful in unforseen ways. At one point, I was considering a fork of putty to add support for tabs. It has a lot of global state, so I had to abandon that idea. If the same state had been wrapped up in a state struct and passed around, it’d have been easy.
I would like to know more about this. I've written several games in Elm, a js counterpart to Haskell, and I guess foolishly assumed that meant I was writing them in the functional style. Now I'm curious if I'm actually writing in an iterative style and if so what would the functional style look like? I would welcome general responses to this as well as any comments on the code style specifically in examples like [0].
[0] https://github.com/tristanpendergrass/legendary-barnacle/blo...
int x = foo();
bar();
char y = baz(x);
and do
x <- foo
bar
y <- baz x
I'd say the true difference is more related to how you describe state change. Imperative is about having a sequential list of instructions that all can mutate global state in some way, whereas with a functional style it's about composing functions that depend on their arguments rather than global state. Functional purity lends itself to a functional style, but you can just explicitly pass a large (global) state around and compose functions only sequentially, which imo lands you right back in imperative territory.When you use IO with do notation, you are actually doing imperative functional programming.
Trying to wrap an entire function's computation into one expression is a fun mental exercise, but the resulting code may be harder to read than a sequence of statements. Haskell's "do" notation seems to be an acknowledgment of this, but to me it's adding another layer of abstraction just to recover what imperative syntax gives in the first place. Why not start from the imperative model, and then find other ways to try to limit side effects and mutation? Languages like Rust may be heading in that direction; "Return of the statement" as it were.
This video gives a bit of discussion around that point: https://www.youtube.com/watch?v=iSmkqocn0oQ
My take on it is that it's too hard to put the side-effect genie back into the bottle once you take it out. It's hard to write side-effect-free code out of side-effectful pieces.
E.g. You can't make a reasonable transaction system if you allow side-effects. You just have to hope that the programmer doesn't include any code that can't be rolled back.
> Haskell's "do" notation seems to be an acknowledgment of this, but to me it's adding another layer of abstraction just to recover what imperative syntax gives in the first place.
Haskell do-notation and Scala for-comprehensions kick arse and I always miss them when I don't have them. I think lack of do-notation is my biggest complaint about Java syntax.
Absolutely. Some problems are just so much simpler in an imperative style that it's worth the lack of confidence you get with the functional solution in its correctness and robustness.
That's why I love languages that have fairly powerful functional features but have an imperative escape hatch. I think more and more languages are becoming that. Rust does it very well, but the lisps, probably Scala, and a few others I'm not thinking of may be better, don't have much experience with those though.
For an entity system ETS could be interesting, but it involves copying data on every read/write which could be a problem in games...
I bet you could even write a fantastic AAA game by carefully dropping into NIFs as necessary.
I think though my argument is that you also wouldn't use objects for that sort of thing either. The brittleness of the code has nothing to do with the underlying vm, or its performance, or even it's concurrency, and everything to do with the code architecture and data organization, which is isomorphic between genservers and objects.
Accidentally interleaved mutation is not a theoretical or academic problem. It's probably the number three source of production bugs in my dayjob's various products, behind misunderstood requirements and web browsers constantly changing the rules. It turns out that everyone, even people like me who know better and have been burned several times, will sometimes take the convenient shortcut. It's so tempting to get something done immediately by quietly mixing some mutation in an unexpected place, and it usually doesn't bite you. Then it gets ossified that way, and a year later starts biting you. This really does happen in code maintained by multiple developers over multiple years.
To be fair to the original post, though, Haskell has come a long way in making that kind of coding easy since 2007. The lens library didn't exist back then, and it's a big part of why data transformation programs are so much more pleasant in Haskell than most languages. It lets you express data access at the right level of abstraction. You get laws to enable algebraic reasoning and a broad range of utilities for composing small pieces together to solve complex problems.
> When you start thinking about running, say, all the characters in a game world in parallel, it starts sinking in that the object-oriented approach of updating objects has some deep difficulties in parallel environments. Maybe if all of the object just referenced a read only version of the world state, and we copied over the updated version at the end of the frame… Hey, wait a minute…
https://gamasutra.com/view/news/169296/Indepth_Functional_pr...
But in ECS engines you still access a portion of global world state you're interested in, and you mutate some portion of it. The ability to declare what those portions are, when present, allows ECS engines to do stuff like parallel execution of systems, and it also prevents a host of data races, but imho there's nothing really functional / pure / immutable about it.
Who told you that? Sure you can simplify it as a function that is operating on a big table but there is no requirement for it to be pure. An attacking Entity A can mutate the HP values in entity B directly.
FP is only modular when you use combinators. If you use closures then it's no longer modular.
f x y = x + y
g y = y * 2
w x = (f x) . g
g and f are combinators and modular and w is the composition of both of those combinators. w = \x -> (\y -> (x + y) * 2)
In this case the above is not modular because it doesn't use combinators. The above style is actually kind of promoted by haskell when you need to do things with side effects. It actually makes FP more complex than it needs to be without improving modularity.But in one example f and g can be reused in other contexts, in the other example the two functions are tied together by free variables. Haskell heavily promotes the latter style with do notation. Any function written in the first style is decomposable into component combinators, any function written in the latter style cannot be decomposed.
> And each of these is messier than it sounds, because there are so many counters and thresholds and limiters being managed and sounds being played in all kinds of situations, that the data flow isn't clean by any means. > What's interesting is that it would be trivial to write this in C.
You could make the same argument about dealing with git and all its crazy branching, rebasing, and reflogs, compared to just editing your source code in place. Or double-entry bookkeeping versus just keeping track of how much money you have. It gets awkward.
I loved the elegance of game programming in Elm a few years back, but the performance was dogshit - in my case at least - so I probably wouldn't do it again.
The real reason to stick to C/C++ in game programming is speed.
There's always going to be limits to what you can do in a browser using Elm, but you may be interested in this talk from 2019: https://www.youtube.com/watch?v=_flFAV0_TeY
Functional programming says values like spawn rate or type should be the output of an expression. Monads try to solve this, but storage must be the output of an expression, and is always more complex than simple assignment.
There is one more secret about programming that functional tries to ignore, that it runs on physical machines. Even though some would like to describe programs pure mathematically, they still are bound by physical mechanisms. Most of those mechanisms are for storing state in memory or transferring state to another location. Abstracting over this fact makes programming painful once performance is taken into account.
More on that here: https://medium.com/weekly-webtips/dysfunctional-programming-...
I personally always shied away from adopting/committing myself to any strict concept. My long experience taught me that as soon as you do there it will likely blow up in your face one or the other way some time down the road.
https://youtu.be/1PhArSujR_A?t=125
https://www.gamasutra.com/view/news/169296/Indepth_Functiona...
For example in erlang/elixir you can keep game state in a series of processes. You model the state changes based on messages that a process receives, and you can spawn/terminate processes as needed. Everything he described as challenging is trivial if you understand the language paradigm.