> admit that functional programming is the wrong paradigm for some types of problems.
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.
I think anyone who has coded long enough would recognize that OOP, functional, imperative, etc all have their strengths and weaknesses, and all have problem domains they are better suited for. Unfortunately, this sometimes gets lost in the language fan wars - it’s easy to lose nuance when discussing something you are passionate about, I have made this mistake myself.
I'd go one step further and say that most people should care little about language features when doing an actual production project, as there are aspects such as how proficient your team is in a given language and how easy it will be to maintain afterwards that are simply more important.
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.
> how proficient your team is in a given language and how easy it will be to maintain afterwards that are simply more important.
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.
I'm all for the 'right tool for the job' mindset, but why would it be bad in principle to write an application in JS? Leaving aside the fact that modern JS is ~3x slower than C++, i.e. slightly slower than C# and Go but in the same league as Java/Haskell, not in the same league as Ruby and Python, programs like VSCode are written in JS and they have no performance issues.
It's true that performance is often ignored for no good reason at all, but it's not such a pressing concern for most applications. Very little software needs to be performant to the point that what language it's written in even matters.
> programs like VSCode are written in JS and they have no performance issues.
here it is compared to qtcreator. it is so damn frustrating when you are used to computers responding near-instantly to have something that... takes its time or stutter to say the least
I really dislike having to use microsoft stuff. No, I don't want to install the app. no I don't want to enable what you think I should enable. No I don't want cortana reading my email messages. No I don't want to use your OTHER product too.
> VSCode are written in JS and they have no performance issues
VSCode is not performant, especially on startup and especially when compared to editors like vim or sublime text. I agree that the reason likely isn't language choice though it's the use of electron. I don't agree with you about software not needing to be performant either, I believe developers should strive to make their software run as efficiently as they can.
> I believe developers should strive to make their software run as efficiently as they can.
This is not realistic. Performance doesn't just happen. Within limited constraints, focusing on performance will necessarily imply less feature.
Ultimately, it's about balance. Personally I find VSC's performance to be good, and at lot of other devs find it at least good enough (it's a widespread editor). I've actually switched away from ST3 because the development of their current version stagnated, and there are unresolved bugs and limitations that affected my everyday usage.
In other words, the performance/features (and let's throw flexibility) balance of VSC is, for me, better than ST's (others may have have different requirements and different balances, of course).
Ultimately, VSC is a bad example of tradeoff performance/ease of (internal) development. If people wants to find an example, Slack is by far the best (worst).
> This is not realistic. Performance doesn't just happen. Within limited constraints, focusing on performance will necessarily imply less feature.
I don’t think this has to be the case. I’ve found that it’s not really that much harder to develop in a language like C++ compared to JS. You can develop the same feature set using better tools and end up with a more performant product (plus you get a typechecker). Performance is a feature, and if you use good tools and a language that enables efficiency without much effort, it doesn’t have to be a fixed cost added on to development.
Writing similar imperative code in C++ as you would in JS with no effort spent on optimizing is almost guaranteed to be much, much more efficient. I guess my point is that to some extent performance vs. more features is a false dichotomy.
> Personally I find VSC's performance to be good, and at lot of other devs find it at least good enough (it's a widespread editor).
That's a part of the problem. We're so used to web apps that many of us just learned to live with constant delays and simply treat it as a part of our lives.
Thing is, I'm not advocating for performance over functionality or premature optimisation. We're talking about text editors here, as an industry we've done this before, we can write a fast cross-platform text editor, it feels like we've either forgotten this or don't care.
The thing that matters when engineering a solution using software is meeting the specifications. There's no real reason to look at the efficiency when it's not affecting whether you meet the specification.
But that's a difference between developers and engineers
Even if it's suboptimal from a technical standpoint, a company that went all in on an HTML/JS Frankenstein is likely to have seen more success than one that went all in on a more appropriate native desktop client. There's a reason we see a lot of the former and not much of the latter.
It's lowest common denominator thinking, which isn't necessarily wrong. If the majority of cross-platform UI code is done in HTML/JS and the toolchain is mature, that becomes the default choice and you would need a compelling reason to not use it. And that has generally been my anecdotal experience.
I’d agree with this. In my experience, choosing a language/platform based on the teams experience instead of the best tool for the job is a siren song. It’s less pain upfront, but way more amortized pain over the life of the project.
Start with good engineers who understand CS fundamentals. After a week or two of immersion, they’re passable in a new language. After a month or two, there’s basically no difference with a veteran.
Okay, sure there are exceptions with very different paradigms. Kotlin devs may take more time with low-level memory managed C code. C hackers may struggle with Haskell. But Pythonistas can write pretty good Typescript in their first week.
In contrast starting with wrong tool for the job, just gets continuously worse as the project progresses. The friction of constantly cutting against the grain is an accumulated drag. As the project grows more complex, the kludges and workarounds become an increasingly heavy burden on a day-to-day basis.
In agreement and I'd add that I think it is a bit arrogant to assume a more junior programmer or a team of them can't learn better programming practices.
A good lead will demand good code and support it with mentoring, pairing and time to refactor.
It still doesn't give them license to do tech for the sake of tech. Every architectural choice and added complexity has to be justified by the value it brings.
But programming for the lowest common skill level on a team is generally a bad idea.
You might learn 95% in two months, but the last 5% might take decades.
One fun thing is that the less you know the more productive you are. So a good strategy is to leave before you learn how horrible your code is, don't stay longer then two years so you don't have to deal with maintainability, performance, scaleability, security, etc.
Yeah but the question is which one of these types are more "program-y"
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.
So, you think (mainstream/message passing) OOP is more extensible / readable / interoperable than FP? Have you ever developed any non-trivial program in FP style?
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)
I think it's time we stop using the phrase "functional" to describe this paradigm; at this point, every imperative language made in the past 30 years has first-class functions, and there is no other first-class language feature common to all the self-described "functional" languages.
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.
Lisp runtime implicitly runs each top-level expression as statement. When you run (def x y) it's not only true that this expression returns 'x, it's also true that it alters the static state of the program by adding 'x to the scope.
To make a purely statementless language, I think you need to make everything happen in a single monadic computation. E.g. in pseudo-Haskell-ish:
main : Scope -> StaticIO Scope
main scope0 = do
scope1, x' <- def scope0 "x" Int
scope2, xv <- dec scope1 "x" 3
At least in all of the discussions I have seen, the difference between statements and expressions is whether they return a value, not whether they have side-effects. So (def x b) is an expression, because it returns a value. {} in C is a statement, even though it has no side effects.
Now, there are some statements in CL as far as I remember - I believe (declaim x integer) is a kind of statement, at least in the sense above.
But you can write a CL program that doesn't use a single statement.
How do any of those qualify as statements? Statements represent actions in imperative programming languages. Declarations and definitions are not actions.
I think it's called functional because it emerges from the lambda calculus (truly just lambdas at the bottom) – but note this definition excludes javascript-with-immutability as well as clojure, not sure if lisp counts
They all count. It's like an athletics competition: everyone is an athlete even if not all people compete in the same discipline, or reach the same performances.
Functional programming was never about first-class functions. It was always about programming with pure and total functions in their mathematical version.
This may be true in your personal experience but that’s certainly not universal. A key distinction is when you learned this: 2-3 decades ago when most people were using languages without first-class functions, this was very commonly the most important feature mentioned because it was the most pronounced difference. That’s no longer emphasized as much now that this feature and related techniques has become mainstream.
I believe the statement GP made has been true since the days of Church.
It is not surprising that "what's the difference between your language and mine" will yield an answer that is different than what you'll get if you ask "what is that concept you're trying to build a language after?"
That might be pointing out an important distinction: did you learn this formally from that academic community or elsewhere? As a global assertion it’s wrong but it could be accurately representative if you’re only talking about subcommunities in the former case.
I have made another comment in this thread where I make a distinction between what people _try_ to achieve, and what tradeoffs they make on the way.
You are right that most functional languages do not have pure and total functions. However, the inspiration they take from is usually the pure/total functions as described above.
> It was always about programming with pure and total functions in their mathematical version.
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.
Several recent languages that came from the FP community recently tend to achieve this. This is the case of Agda, for instance, and I believe Idris also has totality checking.
Maybe you mean optional totality? As in, a function can be declared to be total and the compiler tries to prove it?
A language cannot be Turing-complete if it completely forbids non-termination, there is no way around this. Or stated differently: If a language is Turing-complete, then there are programs that don't terminate. If a Turing-complete language only has functions which are total (i.e., which always terminate), then it has another mechanism which allows non-termination, e.g. unbounded loops and mutable state.
> A language cannot be Turing-complete if it completely forbids non-termination, there is no way around this.
I do acknowledge this. My point is that turing-completeness is not necessarily a desirable aspect of a language. You can check out Agda if you are interested in what can be done with a total language.
For some use-cases, like seting up a mainloop, you need an escape hatch because that mainloop is not going to be total. But not every program needs that. Conversely, a programming language that doesn't care about totality won't be able to sere other use-cases, such as building proven programs. My understanding is that it's much easier to build an escape hatch in total languages, than it is to retrofit non-total languages to prove programs.
> My point is that turing-completeness is not necessarily a desirable aspect of a language.
Ah, okay, I see.
> For some use-cases, like seting up a mainloop, you need an escape hatch because that mainloop is not going to be total. But not every program needs that.
Hmm, I would argue that in practice, nearly all programs will need that, because they're either potentially non-terminating (let's call this "type 1") or have a runtime with a bound which is not determined by any of its inputs ("type 2"). Examples for type 1 include firmware, kernels, drivers, database engines, any kind of server, all programs with a GUI, all REPLs, and many Unix-style utilities that process or generate streams (thing grep, cat, sed, dd). Examples for type 2 include all programs with non-blocking IO (because even if the input and output are known to be bounded, the number of read/write calls is potentially unbounded) and most programs which take a file name as input (because the file size is not part of the program's input).
That doesn't leave many exceptions. I admit, however, that in most cases you could structure your program so that it has a non-total mainloop which exclusively calls proven total functions. But I don't think this is very useful, because even in safety-relevant, hard real-time contexts, termination is not sufficient, but termination within a certain timespan.
Examples of programs which are useful and terminate are batch programs. Besides that, even in a server, you may want to ensure that a subset of the program terminates. For instance, if you're using a serverless architecture, knowing that your lambda will terminate may be useful.
Much like the separation between pure and impure functions in fp, having a distinction between total and non-total functions means that you can limit your non-total code to a couple dozen LoCs, which is a reasonable thing to manually check.
> But I don't think this is very useful, because even in safety-relevant, hard real-time contexts, termination is not sufficient, but termination within a certain timespan.
Once you've broken the non-termination issue, it becomes much more reasonable to start working on time estimates. Basically, once you've said that the number of instructions needed to solve the problem is a function of your input size, calculating that function becomes a possibility.
That's not really true. Op is programming in erlang, which has never in it's history been pure functional, and specifically picked the functional paradigm for pragmatic reasons (function-internal immutability and limited global state gives the programmer guardrails), not for pure mathematical reasons.
Eh, syntactically it hides the state as in `f x` has the same value every time, but calling functional programming stateless is a stretch, I consider it a half-truth told the newcomers to understand the concept. Functional languages still have state but they're abstracted out to function call. E.g. if you have a recursive descent parser written in Haskell, your program will still have state, not every time you're in `expr` function you'll have the same conceptual state.
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.
In addition, every closure is also a 'state container,' as it were. This is how some functional languages e.g. Erlang emulate mutation. A function holds the desired state in a closure and evaluates to a new function closure with updated state. It's also how a stateful object system can be trivially implemented in Scheme.
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.
You can use closures to store state in Erlang, but generally state is kept by passing it through to further function calls.
I would say this makes Erlang mostly have explicit state, rather than saying Erlang is stateless.
Ets is implemented in C as global mutable state, but you could implement it in Erlang as a process per table (and a process to hold the list of tables) with no loss of functionality. You would send messages to fetch data or update data, etc. It's a performance and memory efficiency optimization to do it with C, of course.
Pure functional languages, aka Haskell or Idris or whatever, don't have this property. A closure closes over values, and thus is a value, no state mutation involved.
Of course, your point about complex mechanisms to reintroduce mutation into specific contexts stands (usually via monads) but those are a very different Kind of object then the functions.
It’s important to note Erlang’s global state tools are about controlled global state. The process registry is more elaborate than a simple naming scheme in say Python or Java but really not much harder to use. However that controlled registry allows the process registry to become multi-node with minimal or no code changes for the clients. It’s comparable in power to but far simpler than Consul or etcd for finding services. I view micro services as fairly equivalent to Erlang actors as they both reinforce that state encapsulation. The same idea goes for mnesia or ETS which are comparable in power to Redis.
Before reading this article and its follow-on, I would have completely endorsed the view that global mutable state is the worst, but for the sorts of issues the author is writing about, it seems that global mutable state is precisely the thing that makes procedural solutions more straightforward. In this case, it seems to be an intuitive and straightforward model for how both the programmer and player view the problem.
This is true to the extent that it’s true to say that functional programming requires you to make copies of immutable structures every time something changes, which is to say not at all.
Multithreading is a problem when you have chaotic ownership of writes and updates. There are decades of prior art showing that this is a manageable problem using various strategies, and it’s not helpful to talk in absolutes rather than discussing the relative trade offs.
When your problem domain has changing global state, do you not have synchronization issues - ensuring that stale data is not used, for example - regardless of how you program it? I recall an article making this point, possibly by the same author.
The article didn't talk about global mutable state. Nobody likes global mutable state. Nobody, not even C programmers. If possible mutable state should always be local to a function.
> there is no other first-class language feature common to all the self-described "functional" languages.
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.
I agree. Also in a practical sense, functional paradigms are sometimes or often implemented with a single global mutable variable as a sort of database. This pattern is quite typical for both Clojure programs and JavaScript frontend programs.
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.
> Functional programming as a concept is well defined, and it's not merely "first-order functions".
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"?
Functional programming languages, more often than not, distinguish themselves with some combination of the following features:
* First-order functions
* Partial function application / currying
* Type inference
* Algebraic data types
* Typeclasses
* An emphasis on pure functions (side effects must be explicitly annotated), opening up more possibilities for static analysis
* Lazy evaluation
This is not to say that the above features cannot be emulated by other languages. For instance, Rust has traits, which serve more or less the same practical purpose as Haskell typeclasses.
However, FP is designed to prioritize the above features, whereas other languages may not be.
> However, FP is designed to prioritize the above features, whereas other languages may not be.
This statement is pretty vauge and further supports kibwen's claim that a term "functional programming" ceased to be meaningful or is in the course of being so. I can't see the reason that Rust is not as functional as OCaml by your standard, as both fill four boxes out of seven (only OCaml has currying, only Rust has typeclasses). It can be argued that Rust was not originally designed as functional one, but that would be absurd because it doesn't relate to the current language.
This quote elsewhere in the thread lines up with my knowledge:
> Functional programming was never about first-class functions. It was always about programming with pure and total functions in their mathematical version.
Other people have correctly pointed out that this is an extremely ambitious idea, since, as they have noted, you can't build a turing-complete language with pure and total functions.
This very high bar explains why there has been so many different design decisions in languages, and why most fp languages make tradeoffs in order to reach a compromise between what they believe is important in that concept, and what they can pull in terms of usable software.
As I noted as a response to that thread, we have seen in the recent years languages that achieve pure/total functions, but this was only possible because so many forebearers had tried different designs, and comprmised to produce usable software they could iterate from.
> 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.
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.
"Functional programming" is a useful description for techniques/approaches. The unifying principle tends to be (a) semantics based on definition and substitution of values/expressions (e.g. no ambient state/context) and (b) 'direct' evaluation strategy for expressions (e.g. no search, like in logic programming).
"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).
Ever heard of the Elm Architecture? It helps address some of these pain points, e.g., by forcing you to decide from the get-go what your global modifiable state comprises, and then having each action consist of an update to that state model.
>Sure, there have been games written in Lisp and some games written by language dilettantes fond of Objective Caml, but they never turn out to be programmed in a functional style. You can write imperative code in those languages easily enough.
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].
My understanding (which there's a good chance is incorrect) is that all code written in Elm has to be in a functional style because the language strictly enforces it; but this enforcement doesn't exist in Lisp and OCaml, so they can be used to write imperative or object-oriented code, as happened in the cases described.
Haskell enforces purity, but you can still write pretty imperative looking code by using monads (e.g. some state monad). I don't think there is any philosophical difference between say
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.
How would you handle state other than passing it around?
The fundamental thing that Haskell and Elm do is that they don't have mutable values. They create a new value from the old one. You never mutate a record the way you mutate a JS object or a Python dict.
> How would you handle state other than passing it around?
By only passing and updating the relevant bits of state.
>The fundamental thing that Haskell and Elm do is that they don't have mutable values. They create a new value from the old one.
If we're talking about the context of a game, you'll be very likely using monads where this isn't the case (IO, ST). Not to mention, even if you do just use a normal state monad, if you don't keep the old value around, there's no functional difference between mutating the entire program state and creating a new program state while forgetting the old.
I'd say there is quite an important conceptual difference in that the state monad example exactly desugars to pure functional code. Monads give us a good syntax for sequential computation, but this sequentiality is built out of purely functional components.
The State monad desugars to pure code, but the IO monad doesn't. If you use IORefs or MVars, then you do actual, real mutability by changing the value at a location.
Your answer may be in the comment just after: in elm you explicit the state, you isolate it and you can reason about its structure, but there still is a state.
I think it helps if you don't think about a single axis (functional vs imperative), but in two axis (functional vs procedural, declarative vs imperative).
When you use IO with do notation, you are actually doing imperative functional programming.
From my, admittedly hobbyist, perspective, I would avoid writing game logic like this without using the command pattern[0]. Whether you're impurely updating values directly or purely inputting and outputting the game state, without commands your function is always a black box. This is more convenient initially but when everything is a black box it's difficult to write 'meta' effects correctly. For example in a card game you'd often have things like 'if this happens, do that as well/instead', do effect X twice, change the target of X to Y, etc. If you diligently implement everything as "state -> [command]" , then meta stuff behaves a lot better, as does undo/redo, logging, and debugging. Note that some might scoff at explicit command structs and write everything as simple closures, but then you get none of those benefits. While the linked example is from an OO language, I think it is more functional than the "state -> state" style, because everything except the commands themselves just does computation to produce values instead of transforming a big state bag into another.
I wrote an exercise once while I was teaching: "pure functional pacman". Really I did it to teach myself Redux, and it was a pretty fun way to learn.
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.
Off topic, but may I ask what USB stack you're working on? Because it might have a chance at being the first USB stack not full of exploitable bugs.
(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)
I'm talking to a bunch of UVC webcams using https://docs.rs/rusb/0.6.4/rusb/ for some conferencing software. So sadly no help for your (very valid) concerns.
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 _really_ struggled to not use contrived examples when teaching OOP. It was very hard because you don't really need it until your application becomes non-trivial so I see why all the college classes use cars & trucks or whatever.
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.
If it helps, IMO inheritance is the least interesting feature of OOP. Programming with interfaces is a much more compelling story. A concrete example that is probably small enough to fit within an article or class is a 2d physics engine, where object positions, velocities and accelerations are updated in a loop.
Inheritance is valuable and gets a bad wrap, IMO. I just think about Django's `Model` class and how much time that's saved me and probably tens of thousands of other people. It doesn't even really seem to have any foot guns to speak of, it's just a great use of the abstraction to build up database models without mucking about in the stuff you wish was already written.
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.
Inheritance is a kind of implicit adapter pattern -- instead of implementing a (complex or low-level) interface directly, you can implement a slightly simpler or better-tuned interface, and the inheritance machinery takes care of adapting that interface to the one you ultimately need.
When used in that context, it's truly excellent. I suspect your example from Django fits this pretty well. But it's far too easy to abuse for general code reuse, which is a deep and alluring tar pit.
Could it be the case that functional structure makes programs more reliable and easier to reason about in the large, but functional style/syntax may not be the best way to write code locally?
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.
I completely agree. Just write pure functions as much as possible and you're good. What goes inside a function doesn't matter so long as the function itself is pure, imo
Haskell has ST[1], which allows you to do mutable in place updates in a way that the compiler can guarantee are not visible from the outside. It's not particularly popular though. Typically it seems to be more convenient to either be thoroughly pure or accept mutability and use IO.
There's also the probably underappreciated packages ref-tf and ref-fd which let you write code that is polymorphic over the type of state reference. These can make 'ST' more acceptable because you don't have to make the decision of what state reference to use up front.
Not really unfortunately. In typescript/javascript you have a few options, but none that are any good.
In js you can freeze an object to avoid mutation, but there's a performance penalty and a lot of hassle. As for side effects, you have the async keyword which often tells you if a function contains a side effect or not.
In typescript you can use readonly for objects and arrays to avoid mutation, but it becomes a hassle for nested objects, and it doesn't work for local state where it's actually ok to mutate things. There is an issue open for typescript that proposes to add a "pure" keyword, but I think it's quite hard to implement.
What I do instead these days is to separate pure from impure by using separate files, but as you noted in a sibling comment, this can quickly fall apart if people aren't onboard with the idea.
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.
I'm confused by this article. The author says he's in erlang (which, btw, is not pure functional). And then goes on to describe a system which sounds like an object oriented system. But a game programmer would probably actually implement it not as object per se, but maybe backed by an entity component system. But it seems like BEAM languages would be actually quite good at expressing entity systems, which, correct me if I'm wrong, are similar to say checking out and manipulating data in a database.
I think it’s a common trap for Erlang beginners to try to represent all entities (player, monster, room, etc.) as processes sending messages to each other, the way you’d use classes in OOP. It’s one way to learn the concepts, but in real apps it leads to messy, brittle code. Erlang processes really shine when they’re used as units of fault tolerance, not a code organization tool.
For an entity system ETS could be interesting, but it involves copying data on every read/write which could be a problem in games...
Sure. You wouldn't use pure erlang for a game where perf is critical, but not every game is like that.
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.
This article implies that functional languages cannot run imperative statements, even though it takes for granted that imperative languages can call functions. Google "world's finest imperative language" for a different take on this.
> 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.
> 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 middle part about insects, their spawn rates, types, and mutating values is all about state. No matter how much you try to go "stateless", there will be some state implied by your data. Functional programming as the author describes it is good at calculating state, but bad at accessing it. The only for functions to access state is through parameters, which have a final single value in your closure (no out parameters please). They don't have registers/variables/properties to modify.
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.
Once you grok the language you can easily model the type of situations that they find awkward.
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.
Honestly I think this conclusion comes more from lack of familiarity with how people solve similar problems with FP than anything. For example, in the author’s followup post:
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.
>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.)
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.
Right but global variables are implicit. Function inputs/outputs are explicit. Another advantage is testing.
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.
> That said, I still think passing the world in and out of every function is too onerous for most real world programs [...]
This is a bit of a strawman. At least in my limited experience, functional programmers think so too. Part of what monads let you do is a kind of inversion of control: instead of changing the world directly (as you would if you were seriously threading the world through your domain logic), you return a description of what you're doing, and let the runtime (in the case of IO) or the interpreter for the monad (in other cases) actually perform the changes.
Similarly, lenses allow you to describe local mutations and lift them to the level of a whole structure.
Explicitly threading your state is the first refactoring, not the final destination. If you have a code smell, the first thing to do is draw it out into the open so you can wrangle it. In languages that don't conveniently support functional strategies like monads or lenses, it's tempting to stop there. But that doesn't mean FP is about stopping there.
You still have to pass your logging code to every function that does logging right? And if you add logging to a function deep in your call stack you have to go all the way up the stack adding it as a parameter everywhere.
It's rigorously correct, but I feel like it's a step too far for most programs.
> You still have to pass your logging code to every function that does logging right?
Yes, but no. Again, there are functional patterns you can apply.
First, functional programs tend to be more horizontally composed than vertically. Side effects usually happen at the same "level" of the application, rather than occuring at multiple depths. So the need to thread logging apparati is already reduced, simply because logging only happens higher up in the application.
Second, because side-effects only really occur at this thin layer, you can capture logging as a monadic action, avoiding the need to explicitly thread the logger state around. (This is the same situation as the "thread the world" problem we discussed earlier.)
Third, because so much more of your program is pure, it is much easier to perform unit tests and check assertions in other ways. At that level, logging is a bandaid over poor testing and assurance, not a goal in itself.
Finally, if you do find yourself deep down-stack with a need to perform some kind of side-effect, you can apply logical methods (a la LVars, CRDTs) to give a stateful interaction between separated parts of your program. The Haxl paper [0] is a good example of this: most of the design is pure functional, but for the part that involve an interaction not easily captured by the fundamental call-return / request-response pattern, they utilize monotonic state to maintain a registry of fulfilled and unfulfilled requests.
> It's rigorously correct, but I feel like it's a step too far for most programs.
I feel you're being too dismissive. There is an entire community of developers -- many of whom operate in industry -- who can and do effectively solve the problems you are raising.
"Rigorously correct" is one of those things non-FP folks think FP is about. You can write buggy code in any paradigm. What people like about FP is that it's easier to reason about, and it's generally easier to find precise abstractions that support that kind of reasoning. "Rigorously correct" is missing the point.
I'm having a hard time reading your question as anything other than "But what if I want to not do functional programming?" As stated, functional programs tend to be shallower. There's less "higher" and "lower" to begin with.
I've written plenty -- I'm not interested in responding to potshots every time.
The point is that you start with your program as a function of the entire state tree, but as you break down the problem into smaller functions they only use or need the subset of the state they intend to “mutate”. In a dynamically typed language, doing this safely requires selecting a subset at the call site (at either the parameter or from the return value). In statically typed languages this is more easily expressed with interfaces(/types/protocols/contracts/whatever the language calls them) and you can still safely pass the full state if that’s more convenient.
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.
Can someone with experience of entity component systems (ECS) please chime in? My understanding is that games are not written in the manner that the author says is so easy, rather they are written as pure functions operating on a big table of world state. The world state happens to be mutable for efficiency, but that's an implementation detail. Then again I've never worked with ECS so perhaps this my misconception.
ECS changes the way you access and mutate the world, compared to classic data graph / reference tree, more classic pervasive singletons ("managers"), or even more classic straight global variables.
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.
>pure functions operating on a big table of world state
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.
Of course it can be awkward as it is very specific and opinionated paradigm. It may help to solve some problems while being totally unsuitable to solve others in a sane manner. There are no silver bullets in life.
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.
The direct update imperative way works until you have enough multithreaded actions that interact in undesirable ways. In addition, locks don't compose. I worked on a relatively straightforward trading app with charting and realtime rates. There were so many bugs from direct updates that interfered with rendering that it was best to restructure it as world-in, apply function(s), world-out. Then the renderer only had to deal with one frame of the world. Some items had a smaller 'world', like a single price tile. Whether done in a functional language or a procedural one this functional pattern is a common one.
I'm not sure I'm understanding the point of this article. Every language and language paradigm is awkward if it's used for purposes it's not meant for. Writing a game is something that is really heavily dependent on mainting some sort of state and functional programming in principle is supposed to be stateless. So, unsurprisingly, it's awkward to use FP for game development.
Functional programs tend to be more modular than imperative or OOP counter parts but nobody really knows why nor do they understand the cases where FP becomes less modular.
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.
Both examples have multiple functions defined as the definition of w.
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.
I would much rather write the logic the author describes in Haskell than C. The C code will have to be carefully managed during development and maintenance to make sure the side effects are happening in the expected order and that no changes ever introduce unexpected interleaving of mutation. The Haskell version will prevent that trivially. You can't even express doing it the wrong way, because you can't mutate anything.
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.
If that's true, why is it an issue John Carmack discusses as affecting difficulty of writing games that work correctly in modern environments?
> 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…
Games usually don't run the characters in parallel for the reasons Carmack stated. I believe he's arguing a hypothetical: if we want to maximize parallelization, what else can be parallelized? What patterns would or would not work in that case?
There was a talk by Carmack where he brings up another challenge with parallelizing characters where two characters want to move to the same location.
So... Most games ignore modern hardware capabilities in order to support a fragile programming model? That doesn't sound like an argument against my point. My point was that mutation makes it harder to get code correct. If the only way you can do it is by refusing to use more than one thread of execution for calculating game state updates, that's not evidence that there's no problem with that approach.
Of course, this does vary heavily by game. There certainly are games that run their world simulation across multiple threads. https://twitter.com/axelgneiting/status/1241487918046347264 is a pretty awesome note about that in the Doom Eternal engine, for instance. But I'm certain getting that to work required very carefully controlling when and how mutation happens.
What is worse is when some developers maintain and mutate global state, write functions with side effects, and still think their code is "functional" because they aren't using any classes. A lot of JavaScript developers are moving to that camp because of React and Hooks.
He thinks passing around the world state as a functional immutable data structure is “gross”, but modifying the same state as a global from anywhere in the program... isn’t?
In spite of its name this video use JavaScript to implement a game using the three styles of programming. Great video learnt from hacker news: https://youtu.be/vK1DazRK_a0
147 comments
[ 3.0 ms ] story [ 188 ms ] threadAbsolutely. 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.
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.
It's true that performance is often ignored for no good reason at all, but it's not such a pressing concern for most applications. Very little software needs to be performant to the point that what language it's written in even matters.
here it is compared to qtcreator. it is so damn frustrating when you are used to computers responding near-instantly to have something that... takes its time or stutter to say the least
https://vimeo.com/410735827
https://vimeo.com/410739729
I really dislike having to use microsoft stuff. No, I don't want to install the app. no I don't want to enable what you think I should enable. No I don't want cortana reading my email messages. No I don't want to use your OTHER product too.
VSCode is not performant, especially on startup and especially when compared to editors like vim or sublime text. I agree that the reason likely isn't language choice though it's the use of electron. I don't agree with you about software not needing to be performant either, I believe developers should strive to make their software run as efficiently as they can.
This is not realistic. Performance doesn't just happen. Within limited constraints, focusing on performance will necessarily imply less feature.
Ultimately, it's about balance. Personally I find VSC's performance to be good, and at lot of other devs find it at least good enough (it's a widespread editor). I've actually switched away from ST3 because the development of their current version stagnated, and there are unresolved bugs and limitations that affected my everyday usage.
In other words, the performance/features (and let's throw flexibility) balance of VSC is, for me, better than ST's (others may have have different requirements and different balances, of course).
Ultimately, VSC is a bad example of tradeoff performance/ease of (internal) development. If people wants to find an example, Slack is by far the best (worst).
I don’t think this has to be the case. I’ve found that it’s not really that much harder to develop in a language like C++ compared to JS. You can develop the same feature set using better tools and end up with a more performant product (plus you get a typechecker). Performance is a feature, and if you use good tools and a language that enables efficiency without much effort, it doesn’t have to be a fixed cost added on to development.
Writing similar imperative code in C++ as you would in JS with no effort spent on optimizing is almost guaranteed to be much, much more efficient. I guess my point is that to some extent performance vs. more features is a false dichotomy.
That's a part of the problem. We're so used to web apps that many of us just learned to live with constant delays and simply treat it as a part of our lives.
But that's a difference between developers and engineers
Start with good engineers who understand CS fundamentals. After a week or two of immersion, they’re passable in a new language. After a month or two, there’s basically no difference with a veteran.
Okay, sure there are exceptions with very different paradigms. Kotlin devs may take more time with low-level memory managed C code. C hackers may struggle with Haskell. But Pythonistas can write pretty good Typescript in their first week.
In contrast starting with wrong tool for the job, just gets continuously worse as the project progresses. The friction of constantly cutting against the grain is an accumulated drag. As the project grows more complex, the kludges and workarounds become an increasingly heavy burden on a day-to-day basis.
A good lead will demand good code and support it with mentoring, pairing and time to refactor.
It still doesn't give them license to do tech for the sake of tech. Every architectural choice and added complexity has to be justified by the value it brings.
But programming for the lowest common skill level on a team is generally a bad idea.
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.
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)
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.
To make a purely statementless language, I think you need to make everything happen in a single monadic computation. E.g. in pseudo-Haskell-ish:
But the main part is still statement.Now, there are some statements in CL as far as I remember - I believe (declaim x integer) is a kind of statement, at least in the sense above.
But you can write a CL program that doesn't use a single statement.
It is not surprising that "what's the difference between your language and mine" will yield an answer that is different than what you'll get if you ask "what is that concept you're trying to build a language after?"
You are right that most functional languages do not have pure and total functions. However, the inspiration they take from is usually the pure/total functions as described above.
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.
A language cannot be Turing-complete if it completely forbids non-termination, there is no way around this. Or stated differently: If a language is Turing-complete, then there are programs that don't terminate. If a Turing-complete language only has functions which are total (i.e., which always terminate), then it has another mechanism which allows non-termination, e.g. unbounded loops and mutable state.
I do acknowledge this. My point is that turing-completeness is not necessarily a desirable aspect of a language. You can check out Agda if you are interested in what can be done with a total language.
For some use-cases, like seting up a mainloop, you need an escape hatch because that mainloop is not going to be total. But not every program needs that. Conversely, a programming language that doesn't care about totality won't be able to sere other use-cases, such as building proven programs. My understanding is that it's much easier to build an escape hatch in total languages, than it is to retrofit non-total languages to prove programs.
Ah, okay, I see.
> For some use-cases, like seting up a mainloop, you need an escape hatch because that mainloop is not going to be total. But not every program needs that.
Hmm, I would argue that in practice, nearly all programs will need that, because they're either potentially non-terminating (let's call this "type 1") or have a runtime with a bound which is not determined by any of its inputs ("type 2"). Examples for type 1 include firmware, kernels, drivers, database engines, any kind of server, all programs with a GUI, all REPLs, and many Unix-style utilities that process or generate streams (thing grep, cat, sed, dd). Examples for type 2 include all programs with non-blocking IO (because even if the input and output are known to be bounded, the number of read/write calls is potentially unbounded) and most programs which take a file name as input (because the file size is not part of the program's input).
That doesn't leave many exceptions. I admit, however, that in most cases you could structure your program so that it has a non-total mainloop which exclusively calls proven total functions. But I don't think this is very useful, because even in safety-relevant, hard real-time contexts, termination is not sufficient, but termination within a certain timespan.
Examples of programs which are useful and terminate are batch programs. Besides that, even in a server, you may want to ensure that a subset of the program terminates. For instance, if you're using a serverless architecture, knowing that your lambda will terminate may be useful.
Much like the separation between pure and impure functions in fp, having a distinction between total and non-total functions means that you can limit your non-total code to a couple dozen LoCs, which is a reasonable thing to manually check.
> But I don't think this is very useful, because even in safety-relevant, hard real-time contexts, termination is not sufficient, but termination within a certain timespan.
Once you've broken the non-termination issue, it becomes much more reasonable to start working on time estimates. Basically, once you've said that the number of instructions needed to solve the problem is a function of your input size, calculating that function becomes a possibility.
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.
I would say this makes Erlang mostly have explicit state, rather than saying Erlang is stateless.
Ets is implemented in C as global mutable state, but you could implement it in Erlang as a process per table (and a process to hold the list of tables) with no loss of functionality. You would send messages to fetch data or update data, etc. It's a performance and memory efficiency optimization to do it with C, of course.
Of course, your point about complex mechanisms to reintroduce mutation into specific contexts stands (usually via monads) but those are a very different Kind of object then the functions.
Multithreading is a problem when you have chaotic ownership of writes and updates. There are decades of prior art showing that this is a manageable problem using various strategies, and it’s not helpful to talk in absolutes rather than discussing the relative trade offs.
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.
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.
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"?
* First-order functions
* Partial function application / currying
* Type inference
* Algebraic data types
* Typeclasses
* An emphasis on pure functions (side effects must be explicitly annotated), opening up more possibilities for static analysis
* Lazy evaluation
This is not to say that the above features cannot be emulated by other languages. For instance, Rust has traits, which serve more or less the same practical purpose as Haskell typeclasses.
However, FP is designed to prioritize the above features, whereas other languages may not be.
This statement is pretty vauge and further supports kibwen's claim that a term "functional programming" ceased to be meaningful or is in the course of being so. I can't see the reason that Rust is not as functional as OCaml by your standard, as both fill four boxes out of seven (only OCaml has currying, only Rust has typeclasses). It can be argued that Rust was not originally designed as functional one, but that would be absurd because it doesn't relate to the current language.
> Functional programming was never about first-class functions. It was always about programming with pure and total functions in their mathematical version.
Other people have correctly pointed out that this is an extremely ambitious idea, since, as they have noted, you can't build a turing-complete language with pure and total functions.
This very high bar explains why there has been so many different design decisions in languages, and why most fp languages make tradeoffs in order to reach a compromise between what they believe is important in that concept, and what they can pull in terms of usable software.
As I noted as a response to that thread, we have seen in the recent years languages that achieve pure/total functions, but this was only possible because so many forebearers had tried different designs, and comprmised to produce usable software they could iterate from.
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.
Literally the same thing.
"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).
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...
The fundamental thing that Haskell and Elm do is that they don't have mutable values. They create a new value from the old one. You never mutate a record the way you mutate a JS object or a Python dict.
By only passing and updating the relevant bits of state.
>The fundamental thing that Haskell and Elm do is that they don't have mutable values. They create a new value from the old one.
If we're talking about the context of a game, you'll be very likely using monads where this isn't the case (IO, ST). Not to mention, even if you do just use a normal state monad, if you don't keep the old value around, there's no functional difference between mutating the entire program state and creating a new program state while forgetting the old.
When you use IO with do notation, you are actually doing imperative functional programming.
[0] http://gameprogrammingpatterns.com/command.html
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'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)
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.
When used in that context, it's truly excellent. I suspect your example from Django fits this pretty well. But it's far too easy to abuse for general code reuse, which is a deep and alluring tar pit.
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.
[1] https://hackage.haskell.org/package/base-4.14.0.0/docs/Contr...
In js you can freeze an object to avoid mutation, but there's a performance penalty and a lot of hassle. As for side effects, you have the async keyword which often tells you if a function contains a side effect or not.
In typescript you can use readonly for objects and arrays to avoid mutation, but it becomes a hassle for nested objects, and it doesn't work for local state where it's actually ok to mutate things. There is an issue open for typescript that proposes to add a "pure" keyword, but I think it's quite hard to implement.
What I do instead these days is to separate pure from impure by using separate files, but as you noted in a sibling comment, this can quickly fall apart if people aren't onboard with the idea.
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.
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.
> 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.
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.
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.
This is a bit of a strawman. At least in my limited experience, functional programmers think so too. Part of what monads let you do is a kind of inversion of control: instead of changing the world directly (as you would if you were seriously threading the world through your domain logic), you return a description of what you're doing, and let the runtime (in the case of IO) or the interpreter for the monad (in other cases) actually perform the changes.
Similarly, lenses allow you to describe local mutations and lift them to the level of a whole structure.
Explicitly threading your state is the first refactoring, not the final destination. If you have a code smell, the first thing to do is draw it out into the open so you can wrangle it. In languages that don't conveniently support functional strategies like monads or lenses, it's tempting to stop there. But that doesn't mean FP is about stopping there.
It's rigorously correct, but I feel like it's a step too far for most programs.
Yes, but no. Again, there are functional patterns you can apply.
First, functional programs tend to be more horizontally composed than vertically. Side effects usually happen at the same "level" of the application, rather than occuring at multiple depths. So the need to thread logging apparati is already reduced, simply because logging only happens higher up in the application.
Second, because side-effects only really occur at this thin layer, you can capture logging as a monadic action, avoiding the need to explicitly thread the logger state around. (This is the same situation as the "thread the world" problem we discussed earlier.)
Third, because so much more of your program is pure, it is much easier to perform unit tests and check assertions in other ways. At that level, logging is a bandaid over poor testing and assurance, not a goal in itself.
Finally, if you do find yourself deep down-stack with a need to perform some kind of side-effect, you can apply logical methods (a la LVars, CRDTs) to give a stateful interaction between separated parts of your program. The Haxl paper [0] is a good example of this: most of the design is pure functional, but for the part that involve an interaction not easily captured by the fundamental call-return / request-response pattern, they utilize monotonic state to maintain a registry of fulfilled and unfulfilled requests.
[0] https://simonmar.github.io/bib/papers/haxl-icfp14.pdf
> It's rigorously correct, but I feel like it's a step too far for most programs.
I feel you're being too dismissive. There is an entire community of developers -- many of whom operate in industry -- who can and do effectively solve the problems you are raising.
"Rigorously correct" is one of those things non-FP folks think FP is about. You can write buggy code in any paradigm. What people like about FP is that it's easier to reason about, and it's generally easier to find precise abstractions that support that kind of reasoning. "Rigorously correct" is missing the point.
But... what if I don't want it to? You can't have logging higher up in the stack if you want to log stuff low down in the stack.
I've written plenty -- I'm not interested in responding to potshots every time.
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.
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.
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.
FP is only modular when you use combinators. If you use closures then it's no longer modular.
g and f are combinators and modular and w is the composition of both of those combinators. 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.
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...
There was a talk by Carmack where he brings up another challenge with parallelizing characters where two characters want to move to the same location.
Of course, this does vary heavily by game. There certainly are games that run their world simulation across multiple threads. https://twitter.com/axelgneiting/status/1241487918046347264 is a pretty awesome note about that in the Doom Eternal engine, for instance. But I'm certain getting that to work required very carefully controlling when and how mutation happens.
More on that here: https://medium.com/weekly-webtips/dysfunctional-programming-...
https://youtu.be/1PhArSujR_A?t=125
https://www.gamasutra.com/view/news/169296/Indepth_Functiona...