top | item 34844716

John Carmack on Functional Programming in C++ (2018)

240 points| signa11 | 3 years ago |sevangelatos.com | reply

173 comments

order
[+] seanalltogether|3 years ago|reply
> A large fraction of the flaws in software development are due to programmers not fully understanding all the possible states their code may execute in.

I think this probably the most important concept for programmers to keep at the front of their minds when working day to day. So many of the problems I see come from developers (myself included) adding new business logic to a process, or fixing a bug and not thinking through the side effects of the change

[+] mightybyte|3 years ago|reply
It took me awhile to realize but this is one of the big things that algebraic data types (ADTs) help you to do...design your data types so they have exactly the number of valid states that your domain has. To use another common FP way of saying it...make invalid states unrepresentable.
[+] HybridCurve|3 years ago|reply
I actually just started to refactor some of my code earlier this week to deal with this very problem.

It was related to deserialized data being transposed from one complex digraph to another. The reason I did not catch it as being problematic sooner was precisely because the state management was loosely distributed within the transposing functions' call hierarchy. The code which was trying to manage the states had emerged from fixes for a series of different problems related to edge cases, and once the new problems started relating to fixes for the others, I had that "Ahhh, well sh*t.." moment you get when finding you were missing something important.

> I think this probably the most important concept for programmers to keep at the front of their minds when working day to day.

The lesson I had learned was to be more mindful when synthesizing a solution and there might be more state interdependence than we expect. Centralizing state management has helped me solve it in this case.

[+] louthy|3 years ago|reply
> adding new business logic to a process,

Adding logic is the smallest part of the problem: adding types, that aren't constrained to represent the smallest set of values needed for the problem at hand, that's the biggest problem. This 'looseness' doesn't show up as a compilation error and looks on the surface to be just fine. Then the real-world comes along and creates a value that should never exist and the code falls over.

Constraining the data by properly defining types is the (primary) way to reduce the possible states an application can get in. This can be via sum-types (discriminated unions), or even making sure you don't use `int`, `string`, etc. and other types that are just one step away from being dynamic.

When doing pure functional programming, along with properly constrained types, you tend to find the code writes itself. What I mean by this is that it becomes very much about composing functions via 'type lego'. Once the types fit, your code works [1]. This is a very profound feeling once you grok it, because it gives oneself a feeling of confidence in ones own code that doesn't happen in imperative land. I will sometimes go days or weeks without even running the code I'm writing because I just have confidence in it.

[1] It's still possible to write logic errors, but they're the only major class of bugs you get really.

[+] jmrobles|3 years ago|reply
The problem is even bigger when you are reviewing PRs, the lack of context is the big problem,imo.
[+] simplotek|3 years ago|reply
> So many of the problems I see come from developers (myself included) adding new business logic to a process, or fixing a bug and not thinking through the side effects of the change

That's a great take, but I'd turn it upside down. Problems come from developers adding new features while not being mindful of the importance of helping out whoever is reading that code in the future to understand what are all the possible states.

Anyone can hack together code that's all over the place, but forcing people in the future to do shotgun surgery is as bad as adding the bugs ourselves.

[+] makeitdouble|3 years ago|reply
I am always wondering how much are we expected to understand the code we’re working on.

It is very easy to just say people should know everything, but in practice people don‘t review every single library, nor defend against every single possible case, and surely don’t keep everything in memory while working on any single problem.

I really can’t imagine someone “fully understanding” how the linux kernel works for instance, or even a full graphic card driver, or the software on a POS works as working knowledge.

So yeah, people cause bugs because they don’t know it all, but I’m not sure that’s saying much.

PS: real progress is probably made starting from assuming people don’t understand, and having frameworks that somewhat keep the system manageable under that premise.

[+] iExploder|3 years ago|reply
this is why I think tests are so vital.

good luck trying to fix a bug in a complex project without doing some sort of test to make sure the change does not break one of the thousands of requirements implements so far.

from my experience fixing a bug without validating the system has a very high chance to spawn multiple bugs found later on...

[+] _448|3 years ago|reply
I think the best way to solve this problem is by writing small digestible message passing functions (like Erlang) and each function written using design by contrast (like Eiffel). That way the situation can be bought under some control when the software scales.
[+] RichieAHB|3 years ago|reply
In terms of applying this to languages other than C++, as someone who’s relatively new (4 years) to Java I expected to see a lot more side effects in the Java I’ve worked with in that time. In fact, the opposite has been true, and much of the points discussed in this article seems to have been adhered to; even going back to some of the older codebases. That said, we mainly have stateless microservices running on k8s, so naturally that leads to a very different mental model than long-lived stateful applications.

The thing that really strikes me from the article is the tone in which it’s couched. The pragmatism on display makes it so much easier to read than it could be. Coming from another few years writing Scala and thinking I was a functional programmer (writing hobby projects in Haskell, Elm etc.), I’ve realised I prefer programming in Java in a team (or at least my current workplace’s flavour of Java, and we’re on Java 17); a realisation that still surprises me now. In reality, there’s only so much purity needed to ship even a relatively complex microservice, and Java does that very well with some functional patterns thrown at it.

[+] zabzonk|3 years ago|reply
i am by no means a java expert, but i would have thought that the fact that most objects in java are shipped between functions as references makes purity more difficult to effect and to diagnose than in c++ where everything is by default shipped as a value - of course c++ allows you to change this , java less so?
[+] baby|3 years ago|reply
He talked about it a bit during his lex friedman interview. He said he spent some time there, IIRC, and found it somewhat intriguing, perhaps interesting, but needed to get shit done eventually and so stopped his investigation. Very pragmatic.

From what I can see in this article he mostly seemed to like the purity aspect of FP. It seems to me like a number of FP concepts aren’t really “FP things”, as Carmack points out you can use these patterns in imperative languages as well. Making your own DSL seems to also often be an FP thing that doesn’t need to be an FP thing.

I guess the other way is true as well. OCaml has OOP and support for imperative style (with mutation, early returns via exceptions, etc.)

[+] jillesvangurp|3 years ago|reply
In the same interview he also mentions using python a lot lately and making the point that for the vast majority of code, performance really doesn't matter and productivity is more important.

Reading this article, he's making a great argument for Rust. I assume the article predates rust by quite a bit. But basically Rust is not a pure functional language but still has a lot of elements in the language that add purity and rigidity. It allows you to do low level stuff when you need to but at the same time promotes safety and correctness.

[+] ledauphin|3 years ago|reply
just because a pattern can be used alongside a different paradigm doesn't make it "not an FP thing."

Referential transparency and immutability are fundamental to FP, therefore they are meaningfully described as FP things regardless of who is using them or where.

[+] inductive_magic|3 years ago|reply
>He said he spent some time there, IIRC, and found it somewhat intriguing, perhaps interesting, but needed to get shit done eventually and so […]

I think this resonates with many. My take is: don’t be dogmatic. Regardless of the language, one should write as beautiful, composable and pure as possible - but no more than that. Where only performance matters, oop wins. When clarity is the only priority, Haskell wins. But in reality, we’re rarely dealing with either scenario. We’re somewhere in the noisy realm between, and ought to code accordingly. Fundamentalists rarely make policy with the nuance reality deserves.

[+] bmitc|3 years ago|reply
> but needed to get shit done eventually and so stopped his investigation. Very pragmatic.

That's a strange take by him and on pragmatism. There's also almost nothing pragmatic about C++. Many functional languages are actually quite pragmatic.

[+] actinium226|3 years ago|reply
> but if you do this with typical C++ containers you will die.

Looks left, looks right, quietly closes vscode and rm -rf's the dev folder

[+] manmal|3 years ago|reply
John‘s pragmatic approach to this topic (any topic really) is very refreshing. Game dev is one of computing world‘s battle grounds when it comes to real-time performance and stability, and there’s just little place for dogma.
[+] BlueTemplar|3 years ago|reply
> The pure functional way to append something to a list is to return a completely new copy of the list with the new element at the end, leaving the original list unchanged. Actual functional languages are implemented in ways that make this not as disastrous as it sounds,

I'm curious : how do they manage to not make it (performance) disastrous ? Anyone could please forward me some functional-newbie-level examples ?

[+] jamier1978|3 years ago|reply
When this article was published on his altdev blog it had a big impact on how I wrote code. I really embraced pure functions and tried to think in terms of side effect free code and inputs/outputs.

Got no real data for it but I felt the quality of the code I wrote was much better going forward.

[+] jrvarela56|3 years ago|reply
It has helped me see classes as a group of functions with parameters in common. The constructor helps me reduce the amount of params passed to each function. If I avoid mutating the internal state across function calls, I'm just using them as a way to group functions together (as a module would).

Instantiating and then calling different methods helps me test a process at different points of the executing (testing and debugging). Inheritance helps me clean up stuff that's repeated in specific sub-problems. Mixins/modules help me in a similar way, but for stuff that's reusable in unrelated problems.

I'm writing this with Ruby in mind. Seeing how OOP constructs can help me manage code that's written in a functional style has given me a middle ground: I started as 'FP is superior; use everywhere; avoid OOP languages' and now see the OOP stuff just as tools that can make it easier for me to encapsulate and reuse logic. Obv this comes with footguns: taking outside params in methods, local variables changing behaviours, indirection from inheritance/extension - the list goes on and other languages have their own. There's problems with it but as the author says, these things aren't all-or-nothing - so you can nudge the codebase in the FP/'pure' direction.

"Objects are a poor man's closure.... Closures are a poor man's object."

[+] noduerme|3 years ago|reply
As a 25+ years coder who's very accustomed to thinking in my own ways, I have a problem with functional-purism similar to the problem I had with globals and factory functions. The fact is that most programs have a lot of states and these states have to be represented somehow. Allowing the objects to contain functions that act on those states is a perfectly good analogy for most business logic or game logic you're modeling. Abstracting what you can abstract into atomic functions is great. But so many functions are more comprehensible when they take place bound to the scope of what it is they're acting upon... again, not to produce an answer, but to change/track/maintain state which is a critical part of what people experience when they interact with a program. I don't think that thinking in terms of objects or even global states should be considered a code smell when what you need to do is to get things done. If/when you need to make some functions synchronous across asynchronous systems, by atomizing their logic, then you will hit upon functional programming methods by default.
[+] thom|3 years ago|reply
The argument isn’t that state is bad or doesn’t need to be managed. It’s more that spreading that state over a large number of areas leads to a lot of complexity and cognitive overhead in terms of expected behaviour at any given point of execution. Functional programming gives you ways to be much more explicit about the transformation being performed and the before/after states (except arguably when you start getting overly cute with zippers and lenses and such). To the extent that this approach hasn’t magically transformed programmer productivity or software quality, we know that both approaches are admissible.
[+] mejutoco|3 years ago|reply
> these states have to be represented somehow

In data types!

> But so many functions are more comprehensible when they take place bound to the scope of what it is they're acting upon

To me, the most understandable function (lowest context needed, lowest cognitive overhead) is a black box that takes type x as input and produces type y as output, without side-effects

> I don't think that thinking in terms of objects or even global states should be considered a code smell

Classes can mix state and behaviour. I prefer when state is encoded in types and behaviour in functions, instead of having implicit state changes in a class, because it is more difficult to track, and constraint.

Of course, in the real world, if a library or platforms uses OOP, so be it. I am not dogmatic. But ideally, I avoid OOP because of this implicit mixing of state and behaviour.

[+] valenterry|3 years ago|reply
Please read up on the definition of functional programming (or rather: pure functional programming, since you talk about "functional-purism").

Because, everything you are saying is either forbidden nor discouraged in (pure) functional programming. In fact, it is totally orthogonal.

The point of functional programming is solely to prevent the same code to behave differently just because it is used/called in different parts of the program. You can still use objects, you can still make them have methods that operate on only a limited part of the state and so on. No problem with that, I do it all them time in fully pure functional programs. In fact, I think this is best practice.

[+] optbuild|3 years ago|reply
Why aren't more universities teaching functional programming first to form good mental models for the students and then show them when to use state and when to not.

Why is everyone teaching Python?

[+] bick_nyers|3 years ago|reply
When it comes to an Intro. class, they aren't teaching python, they are teaching how computers think, and how to make them think. Python stands the least in the way. Once you get deeper in the program though, I think you need to move away from Python ASAP. Although, if you have any courses that may not be squarely intended for CS majors (like maybe Math/Engineering/Stats/Bio. etc.), then you don't want the language to stand in the way of the concepts there either.
[+] PhDuck|3 years ago|reply
I think many CS degrees teach FP, I remember learning a variant of SML (mosml) for my first programming course. Now the same university is teaching the same course in F#.

Of course a lot of the data science/machine learning is being done in Python, but that is likely due much of the frameworks/tooling around is Python in general in that world.

[+] zactato|3 years ago|reply
Hasn’t Lisp been the language of choice for CS 101 for several decade at MIT and other universities?
[+] fwlr|3 years ago|reply
At least on mobile, it’s not clear where the blogger’s introduction ends and Carmack’s reproduced post begins. 5 years late to mention it, though. Great article.
[+] Sniffnoy|3 years ago|reply
It's not just on mobile; there really doesn't seem to be any visible boundary there.
[+] jasmer|3 years ago|reply
The psychological aspect of 'purity' in this concept is as interesting as anything.

I think that devs are naturally prone to be obsessive compulsive about such things.

We can see 'defeating the borrow checker' as a 1st order objective.

I think devs fee that 'defeating the borrow checker' gives us much more satisfactiong than 'fulfilling the customers needs'.

There's something a big 'chinse finger trap' about FP that consumes our attention on an impusleve level that is not necessarily consistent with what it actually means to be a good engineer.

This, along with our will to abstract and to want to use 'shiny new things' etc..

[+] juunpp|3 years ago|reply
On the other hand, there are those who are too stubborn to use anything but a dull knife and a broken hammer. There are situations where you can, e.g., make an error state impossible by defining a couple of types, or using one or two lines of generics. But then, what should have been a simple change becomes an ordeal because the other person lacks the background on all of these ideas.

I think all CS curricula should have some sort of class that explores languages like Haskell and Rust, the problems they attempt to solve and how they go about solving them, if only for the intellectual experience. What language you end up using, and what ideas you end up applying, is then up to you on a case-by-case basis.

[+] revskill|3 years ago|reply
FP to me is more about abstract the functionality, or generalize functionalities.

OOP is about abstract the mesaging.

When your object is just a pure function, you achive the ultimate abstraction.

[+] jl2718|3 years ago|reply
I’ve considered a lint rule that requires all functions to be written in Lambda syntax so that closures, side effects, and purity can be explicit. However, I haven’t explored possible effects on compile time, size, and performance. Another possibility is explicit scope closures, like [x,&y]{…} available for every defined scope.
[+] zabzonk|3 years ago|reply
best article i've seen on pragmatic purity - i would only dissent on c++ not providing facilities for pure functions (i don't here mean pure in the virtual=0 sense) - you can, and should, do quite a lot.
[+] jb1991|3 years ago|reply
The language does not offer the features that you were referring to, and that the author of the article mentions. You can write pure functions, but the purity is not checked by the compiler, that is what has been meant here.
[+] HybridCurve|3 years ago|reply
GCC has a pure attribute for functions which is documented as being prohibitive of impure behavior. Though, I honestly cannot say how thoroughly it is enforced or if there are significant benefits outside of the obvious and some minor optimizations which the compiler might be able to perform.
[+] leni536|3 years ago|reply
There is one place where purity is effectively enforced: constexpr functions called in constexpr context.

It's a little bit limited, as it's only enforced for compile time evaluations.