> 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 about this quote often in my day to day programming. Object oriented programming is so full of stateful objects and it's easy for programmers to add listeners to all sorts of events. Even on the cloud side now you have all these micro services and trigger functions and small changes can have cascading consequences.
This has really been hitting me lately, as I try to understand a legacy program whose original authors are long gone. But not in a way that implicates OOP.
The code is written in a functional language, and, while it's not one like Haskell whose compiler enforces any rules about referential transparency, I have yet to encounter a single mutable variable. However, it is constantly poking at a database, and different modules communicate with each other asynchronously over a message queue. And those two things are making it just impossible to reason about program state at a macro scale.
To me, it really speaks to how functional programming is not the panacea it's often made out to be. I'd honestly prefer if it were just some crusty old Java monolith where all the objects are stateful, and have setters for every field, and all that good^H^H^H^H stuff. Because, while that kind of statefulness may not be well-isolated, it is navigable. I can splat-B and cmd-F7 my way into a decent understanding of how the application's state is getting twiddled. With this, I'm having to grep the code and manually sift through the lexical search hits to find what I need to know.
> I have yet to encounter a single mutable variable.
> However, it is constantly poking at a database, and different modules communicate with each other asynchronously over a message queue.
I think you found your mutable variables...
No, I agree, functional programming on its own is not a panacea. Every program has state, even functional programs. FP promotes modeling state implicitly, by representing it in the program text itself (or more precisely, the "continuation" of the program). The evaluator of the program is responsible for managing that state, rather than the program author themself(^).
The problem is that, since the state is implicit, it becomes hard for multiple modules of the program to collaborate over that state. There are ways around this -- for instance, have each module supply descriptions of the changes they want to apply, and have a central authority manage the actual state -- but you ultimately end up with some way to "address" the state, either explicitly or by reference to an entity that owns it. In either case, you have the "address" of some thing that may change over time, and so you've got yourself a mutable cell.
All this to say that FP is indeed not a panacea -- every system needs to have a clear data model. Not only the structure of the data needs to be understood, but also the ways and means in which it is allowed to evolve over time. (Something something Linus Torvalds and Fred Brooks something flowcharts vs. tables something data vs. code.) It sounds like the system you're grappling with has a very loose data model -- I'm not surprised it isn't navigable.
But the core idea I at least take away from FP -- that explicit state should be avoided -- is still vitally valuable, even if you approach it differently in different environments and for different problems. "Out of the Tar Pit" is a wonderful exegesis of the idea. [1]
(^) Does singular "they" become "themself" or "themselves"?...
I find your depiction of FP state interesting as I think of it as the opposite. In my practice of FP, it tends to force me to make state explicit and and available. I follow what you’re saying as well with regard to the continuation of the code, but there’s something that feels very distinct between static and runtime state.
> In my practice of FP, it tends to force me to make state explicit and and available.
Ha, I think I get what you mean. The state that you do have is generally called out more visibly. I do think that's a product of defaulting to state avoidance, since the thing you're avoiding becomes more visible whenever it is present.
I think FP does a good job of pushing as much state as possible into the "state in the continuation" category, but there seems to be a hard kernel of mutable state that doesn't dissolve so easily. On the bright side, that makes for a bright, shining target to aim research at (LVars, CRDTs, logic programming).
> there’s something that feels very distinct between static and runtime state
For what it's worth, I'm only thinking about runtime state here. Can you call out where it seems like I'm referring to static state?
> The state that you do have is generally called out more visibly.
You see, this is the exact opposite of what I was discovering in this codebase. Sure, in Haskell, you have an IO monad that effectively puts bright blinking lights and klaxons over all the state, and a compiler that forces you to use it. But the vast majority of functional-style code is not written in a language like that. It's written in a language like Scala or Clojure or JavaScript that allows you to sneak a side effect into any portion of the call tree. And, by being several layers deep in the call tree, it's not visible. It's hidden, and, when it's being done in the context of a generally functional idiom, it's downright pernicious.
You hit the nail on the head in your comment further up. I did find my mutable variables. My epiphany after seeing this code is that, in a language where the only thing that can enforce true purity (as opposed to simply trying to avoid mutable variables as a general rule) is programmer discipline, the natural equilibrium point is code that still every bit as riddled with state. It just delegates the management of that state out to Redis or whatever.
Discipline is a good enforcement strategy for small individual projects. It's possibly also a good one for open source projects. I don't believe it can work for corporate team projects, because social factors at play generally won't allow it to work. There will always be some business constraint that prompts people to take shortcuts.
That definitely toes the important line between FP as defined and FP as practiced. Although, I’d argue that proper state management requires discipline in all cases. I’ve seen OO codebases that operated from lots of trick mutable shared redis vars as well!
Whilst trying to avoid the "No True Scotsman" fallacy, I'd argue that this system is FP only in name, but not in spirit. Even in Haskell, you can spend all your time in the IO monad and use IORefs as shared mutable cells, but you'd have a hard time arguing that such code is "functional".
> It's hidden, and, when it's being done in the context of a generally functional idiom, it's downright pernicious.
I think what we're seeing is a distinction between _syntactically FP_ and _semantically FP_ qualities. It's easy to apply _syntactic_ idioms obtained from FP, as it allows you to avoid and reduce state wherever possible. However, in a language where mutable state is assumed, and it's your responsibility to not use it, you don't get the _semantic_ guarantees about the behavior of your program.
I don't like having to exercise discipline, because no matter how good I am at it, I'm only a temporary part of any software system. IMHO, the fundamental goal of software architecture is to institute bias directly into a codebase to support the problem domain. The way in which you work with a codebase is informed by how that codebase wants you to work with it: you'll naturally avoid things that are made difficult to do, and prefer things that are made easier to do.
Programming languages are essentially the basement level of any given architecture, because it is nearly impossible to override the decisions your language makes for you. It is almost always going to be easier to use what the language provides you, and if the language provides global mutable state, it will always be tempting to couple two otherwise separate regions of your codebase by a mutable cell. Some languages especially make FP idioms difficult (hi, Java), so you end up fighting an uphill battle -- unwinnable if you're not extraordinarily careful.
> There will always be some business constraint that prompts people to take shortcuts.
To borrow a phrase, I don't think FP can "win" until we deal with the forces that make mutable cells such an attractive choice. There are multiple facets to the problem; it's not enough to just pick languages that make FP the easier option (or mutable shared state the harder option). IMO, we need to have an industrial expectation of domain modeling, and architect our systems specifically with our problem domain in mind, so that problems in that domain -- and expected evolution in that domain -- can be handled not only easily, but intuitively within the set architecture. (Lispers go wild over defining their own language within Lisp for exactly this reason -- but all things in moderation.)
> Whilst trying to avoid the "No True Scotsman" fallacy, I'd argue that this system is FP only in name,
I think that, at least for the purposes of the way of thinking that I am moving toward, it isn't exactly that, so much as that we seem to have hit a point of very vehement agreement, except perhaps for some slightly different coloring on the pessimism/optimism scale.
I agree with you that well-done functional code is much nicer to work with, or that the spot where this code I was working with went off the rails is that it kept departing from functional style when the original authors though it convenient to do so. It's more that I'm discovering that FP has an Achilles heel, and it turns out that it was exactly the same Achilles heel that produced my ultimate frustration with SOLID: In typical usage, it's an unstable equilibrium point. I suspect, in retrospect, that one movement failed and the other is doomed to fail because, as you allude to in that last paragraph, they're both trying to solve the wrong problem.
Other background information here is that I've lately been learning Smalltalk and Rust, and, as a result, seeing how eliminating state is far from the only way to tame it. And I've been noticing that, from a readability and maintainability perspective, many of the most compelling open source codebases I'm aware of tend to be neither functional nor object-oriented.
> as a result, seeing how eliminating state is far from the only way to tame it.
I agree! Elimination is but an extreme form of control :)
I have strong hopes that logic programming will provide the next generation of formal tools for controlling (rather than eliminating) state. My ideal paradigm would be "logical shell, functional core" (swapping out "imperative shell"). But logical methods are (a) unknown, (b) niche, and (c) overshadowed by the barest treatment students receive of Prolog, so there's still a long way to go here.
(FWIW, I think of things like LVars, CRDTs, and distributed protocols more broadly as having a fundamentally logical flavor. See the recent CALM Theorem for more on that.)
I was thinking of “static” state as being the idea of state existing “in the program text” and “in the continuation”. Perhaps I don’t yet fully grasp what you’re thinking of with those ideas of state. Especially given the correspondence between continuation passing style and ANF.
I had seen it as “remaining reduction” which in some sense is embodied in the syntax tree being reduced. So in that sense, it’s kind of static from the written text of the program (though duplication can force that to be a runtime concern).
Oh, I see! Yes, the "continuation" I'm referring to is the "remaining reduction". I like to think of FP programs proceeding by reduction, so there's no need to introduce any auxiliary state just to describe the computation -- it's always present in the program itself.
In a traditional imperative program, the "continuation" is the call stack + heap, both of which need to be introduced and managed separately from the "remaining reduction". In formal semantics, you usually have to introduce a reduction on "environments", which is a tuple of the remaining reduction and the separately-managed forms of state. This, specifically, is why I think of state in imperative languages as "explicit" -- it's a separate entity in the operational semantics.
I think the confusion may be that I'm thinking very much in terms of semantics, and almost not at all in terms of syntax. If you're an interpreter in the middle of executing a program, what information do you need to do your job? (We execute code mentally to understand and grasp it, after all.) In the imperative case, the program text (reduced up to this point) is "not enough" to tell what work remains. In the functional case, you need nothing else. State is a real, additional, explicit thing in the one case, and an illusory, implicit thing in the other.
'tel said the same thing, and I can see it both ways. But I don't think it helps me much to simply state the opposite position -- you don't give me much to reply to :)
>I'd honestly prefer if it were just some crusty old Java monolith where all the objects are stateful, and have setters for every field, and all that good^H^H^H^H stuff. Because, while that kind of statefulness may not be well-isolated, it is navigable.
Am I getting this right? Your issue is a lot of hidden, opaque access to external state, so you think adding even more internal state on top would be a remedy because the stuff you add on top would be navigable?
I think that the code I'm looking at took things that could have been internal state, and pushed them out to external state, and, ironically, ended up making them more hidden and opaque in the process.
And I think that certain classes of old-school Java programs - specifically, the ones that use toolkits like Hibernate - have evolved a typical pattern for modeling external state that makes it relatively more navigable. The state tends to get proxied through these entity object horcruxes that aren't particularly well camouflaged.
That is what I gather as well.
Really the parent wishes not to be interfacing with a database, and I suppose in dreamland the database could be integrated to the programmers IDE.
I get where they are coming from though. Even best case, a db call will add more latency than a getter/setter. They might also be missing the types (depending on what functional language it is).
In general, I find that complicated systems with no owner (i.e. legacy systems) are much easier to understand if they're not constantly calling out to other systems like databases.
The code is written in a functional language... I have yet to encounter a single mutable variable. However, it is constantly poking at a database, and different modules communicate with each other asynchronously over a message queue.
Sounds like Elixir. I'm also on a similar project but I thank my stars that I don't also have to worry about mutable state at the micro-level, and that I can chisel away at my small corner of the monolith with blinders on and not worry about messing up other corners of the codebase via spooky action at a distance.
Sometimes, your domain/business logic itself is just really complicated and can't get simplified without domain level (re)organization and that's hard as feature requests pile in and make your codebase more and more complicated.
1. Messaging queues are stateful constructs. That doesn't make them a bad choice, they are much better than callbacks. But they do have state which is always "your problem".
2. "constantly poking at a database": Sounds like bad application design. Interactions with the databases should be isolated to "one corner" of the system.
They've also mentioned cmd-F7, though, and that is the same key you are referring to. It strikes me as odd that they would call it splat and then cmd in succession, if that is what happened.
> easy for programmers to add listeners to all sorts of events
ive seen this in a lot of frp code bases as well though... "event subscribers up the wazoo" is what i would call it, and overdoing it leads to the same tangled mess of "i cannot find out what is doing what when and from where" debug hell...
Object oriented programming (the good parts) also enables enforcing complex data invariants that make it easier to have narrow function contracts since rather than having a function take a type that encodes its invariants directly rather than indirectly.
> A pure function only looks at the parameters passed in to it, and all it does is return one or more computed values based on the parameters. It has no logical side effects. This is an abstraction of course; every function has side effects at the CPU level, and most at the heap level, but the abstraction is still valuable.
Rereading this for the umpteenth time, this is the paragraph which sticks out to me this time around, and a point which I think most people miss, specifically the latter part about “pure” functions consuming CPU and memory.
The reality is that there is no such thing as a “pure” function, insofar as execution of that function costs something. Certain functional programmers corrupted this idea to somehow mean pure functions are free; that these functions can be executed one to infinity times and the result is the same.
For instance, this idea taken to an extreme has resulted in the current situation with React.js, where most React applications pathologically over-execute the “pure” rendering functions which we use to define components, resulting in degraded performance, increased GC pressure, and a lack of understanding of how our code executes.
Just because you can call a pure function many times doesn’t mean you should.
> Certain functional programmers corrupted this idea to somehow mean pure functions are free; that these functions can be executed one to infinity times and the result is the same.
I think you are conflating two things and attacking a strawperson. As someone who is very involved in the functional programming community, I've never heard anyone say that pure functions are "free", and if this has been said it's certainly not the majority perspective. The fact that a function gives you the same result given the same arguments is not meant to be some kind of encouragement to redundantly call a function many times. It's meant to give you a tool to better reason about the program logically. You don't have to worry about _when_ a pure function is called, because it doesn't depend on hidden state that may change over time, and it doesn't change anything about its environment.
In a sense, I think you seem to have been given the _opposite_ idea (not necessarily saying it was your misunderstanding vs. someone else poorly communicating to you); the fact that a function always gives you consistent results means that you never have to invoke it multiple times. In some sense, this viewpoint recognizes the cost of function execution even more than the zeitgeist.
I’m assuming the “free” part is referring to referential transparency. I may have wildly misunderstood that and they may be even more mislead. But when referential transparency is assured, one of the promises of FP is that repeated calls to expensive functions can be optimized. Like I said in my sibling comment, the important part of that promise is can. If a runtime or compiler is able to flag a pure function as expensive, it can automatically memoize it (of course then trading memory space for computation time).
> The fact that a function gives you the same result given the same arguments is not meant to be some kind of encouragement to redundantly call a function many times.
All pure functions are by definition idempotent, and this quality is often celebrated as yet another point of merit. What is the point of highlighting this quality if functional programmers do not expect to over-execute them?
> It's meant to give you a tool to better reason about the program logically. You don't have to worry about _when_ a pure function is called…
If you say you don’t have to worry about when a function is called, the implication is that you don’t have to worry about how many times that function has been called. Functional programmers tend to belittle this sort of reasoning, and yet this sort of reasoning continues to be necessary when evaluating long-running, interactive programs for correctness.
This downplaying of executional reasoning is precisely why even if using pure functions on their own doesn’t encourage overexecution, the systems which functional programmers build typically do. We see this most clearly in discussions of reactivity, the fetishization of spreadsheets as programming model. Discrete events are tranformed into discrete outputs in a many-to-many relationship and yet functional programmers continue to think it’s not important to reason about the code which executed in between, the when and how much, just because it’s a composition of pure functions.
init() { current_operator = ADD }
evalOp(int a, int b){
if (current_operator == ADD) return a + b;
else if (current_operator == MULTIPLY) return a * b;
}
setOp(enum op){ current_operator = op; }
}
// pure impl
class bar{
evalOp(enum op, int a, int b){
if (op == ADD) return a + b;
else if (op == MULTIPLY) return a * b;
}
}
In the first case, evalOp is not pure since one has no idea on what is the current_operator set as. In the second case it is pure since the operator is part of the input.
Purity refers to reproducibility, as in no matter what else is going on, that function will always behave the same. You can have pure functions in OOP based languages, but in practice devs just default to managing state and having weird behavior depending on outside state of a given function.
As a side note, and since you mention runtime performance cost: if you implement a dummy generic cache layer and you have pure functions you will have an easier time than having stateful impls. In this case, that cache layer must have the ability to introspect into foo, but in bar that cache layer just keeps track of input and that's it.
class foo {
public:
//enum is a keyword, so let's call it _enum
const _enum current_operator;
foo(_enum co) : current_operator(co) { }
// ... evalOp() as in your stateful impl example ...
};
//Example use:
Reduce(foo{ADD}, 1, 2, 3, 4, 5, 6);
Point being, functional purity doesn't necessarily mean only functions taking all their inputs as arguments. It means no mutable state. In the above example, the class foo essentially represents a partially applied evalOp(). If you have multiple, related functions working on similar sets of parameters, you could put them in such a class with const members to create what is essentially a package of partially applied functions.
Pureness also makes it easier to remove calls to a function. If f is a pure function, then I know that if x, y or z doesn't change, then I never need to call f(x, y, z) again.
You are correct in that there is a definition of idempotence that agrees with you, but you are wrong to correct the parent comment, because the most common mathematical definition of idempotence of functions agrees with them (and not you).
There are multiple definitions of idempotence, used even within computer science, and neither of you seem to be aware of the definition used by the other. The common mathematical definition of an idempotent function is that f(f(x)) = f(x) for all x. But in computer science there is another common definition which involves side effects not being repeated (that is, `f(); f();` is the same as `f();`).
> But in computer science there is another common definition which involves side effects not being repeated (that is, `f(); f();` is the same as `f();`).
It’s used commonly when working with unreliable communication (e.g. networking). For example, if you make a request but you don’t get a response, either the request or the response might have failed to send. It’s convenient if you don’t need to determine which case occurred; idempotent functions free you from worrying about whether the requested action was carried out.
> Certain functional programmers corrupted this idea to somehow mean pure functions are free; that these functions can be executed one to infinity times and the result is the same.
I'd be very curious to read someone claiming anything of this sort. One benefit (with respect to the CPU) of pure functions is that they can be cached because you know you'll get the same result for each call (of course, this impacts memory), which you can't guarantee with impure functions. But that's a tradeoff of memory for time. I've never seen anyone claim there's no cost to a pure function.
When we say “pure functions are free” out loud, of course it seems categorically false, and I don’t think anyone has ever credibly argued for this claim. And yet I think it is an implicit assumption of a lot of functional programming development, specifically that which attempts to build “referentially transparent” systems on top of an imperative, impure systems and languages, rather than starting with a new language from scratch.
My example of React.js is the pinnacle of this, all features like hooks, contexts, and concurrent mode only make sense when they estimate the cost of re-executing pure functions to be free, or at least negligible, and yet real-world experience indicates this is definitely not the case.
> One benefit (with respect to the CPU) of pure functions is that they can be cached because you know you'll get the same result for each call (of course, this impacts memory), which you can't guarantee with impure functions. But that's a tradeoff of memory for time.
In my personal experience, I’ve become pretty skeptical of trading memory for execution time. Especially in garbage-collected languages, what you save in execution time you lose in GC pauses, so the end result is you get neither.
Caching is and will continue to be a core building block of performance in current sw (and hw) systems. And it's a boon when you have a tool to fix one of the two hard problems[1].
[1]
The famous quote:
"There are only two hard things in Computer Science: cache invalidation and naming things." (attributed to Phil Karlton)
There are some wildly imaginative benefits of FP sold in words by some advocates, oftentimes imagining what could be in a runtime or compiler based on the maths of what’s promised by the practice. It’s easy to imagine that if an expensive function call always produces the same results it can be optimized to a single call, after which all further calls are free (a memory access). It’s easy to then overstate or imply that calling a pure function guarantees that kind of optimization even if it doesn’t.
You are right of course, that this is not always implemented. I'd add though, that an impure implementation would never allow for such an optimization. The rest is the job of compiler developers. Of course you need to keep an eye on runtime execution cost as well. That is part of your job.
> One benefit (with respect to the CPU) of pure functions is that they can be cached
Only if the input space is very small. If your function takes as input two booleans and a string you know is an ID from a relatively static and small set of IDs, memoization is the way to go. If your function takes in two arbitrary floats, trying to memoize anything will only make your program run slower and leak memory.
Caching the result of common inputs and discarding not-recently-used results can be useful on functions with huge - but nonuniformly distributed - input and output spaces. At the extreme, you might have procedural image or texture generation - perhaps the postprocessing of another image or texture, for example. Impurity is a great way to end up with serious cache invalidation bugs when untracked inputs are changed, and purity a great way to avoid them.
Well, caching is a thing that shouldn't be done "just because" but after some analysis that says it's worth it. And as MaulingMonkey said, a cache doesn't need to keep everything, just enough things to improve performance. And if the cache is costing you performance versus recomputation, then don't do it. I mean, just like anything else in life, if it hurts don't do it. If it costs you performance in a way that you care about, use an alternative.
The "free" part you're describing is called referential transparency. What it means is that a pure function, called with the same parameters, always returning the same results without side-effect, can be substituted with its return value as an assignment (or as part of an expression).
The most important part of that explanation is can. The idea behind certain properties of FP, this being one of the most important ones, is that you are able to describe the results you want (somewhat declarative, moreso than imperative languages, less so than logic programming) and let the "compiler" or the "runtime" determine how that's delivered, without knowing or caring how it's executed. The problem with that is that the "compiler" or the "runtime" has to actually do the optimization, and if it doesn't, you're... executing unoptimized code!
Taking your React example, if it had appropriate heuristics, it could noop a ton of work and devs wouldn't have to even think about optimizing really simple good hygiene patterns. But it doesn't even look at your source code, it has no idea what weird stateful complex mess you're calling into, and it just assumes the worst.
If you’re interested in a language which does claim to optimize pure functions, take a look at Koka (https://koka-lang.github.io/koka/doc/index.html), a research language from Microsoft. It doesn’t implement any control flow besides conditionals and pattern matching; everything else is implemented with higher-order functions, and a cool syntax which allows trailing callback parameters to look like a block of code makes it look almost imperative. Additionally, they have a complex effect tracking system which tracks not only if a function is pure, but also things like if it might be non-terminating or throw errors. Somehow this is used to transform functional code into code which is about as fast as the imperative equivalent.
Ultimately, I think reasoning about function effects in such a fine-grained way probably won’t catch on, but it’s a great example of what you can do when you design compiler up. However, the original article, and most in production functional systems, are about attempting to carve out a functional system from imperative parts. The result is like you said, we’re executing unoptimized code.
This is why (the keyword) pure in D (mentioned in the article) asserts that the function does the same thing for the same input - not necessarily the same output. Side effects are still reasonably possible, just not launching the missiles.
Strong purity can be checked by having immutable arguments.
A true pure function combined with persistent immutable data structures (Immutable.js, Clojure, Scala, et al) can be memoized and re-executed practically for free (in very short constant time).
P.S. In React for example, in almost all cases the amount of memory dedicated to holding immutable data is typically tiny for modern hardware. So it almost always makes sense to trade memory for faster execution.
That post is on multiple websites and I think it's one of the best written one by Carmack because it hits home directly (it applies to software as well as games). I think it gets posted once a year on HN. No regret though I don't mind an echo chamber of good thoughts.
So watched the talk just now. I've been using C++ as my main language since the early 90s and it pains me to see what it has become. I thought that I might've been missing something all along. Unfortunately, I wasn't.
The talk all but confirmed my lingering assumption that C++ is intentionally designed to be an over-engineered language with no particular regard to the elegance of its construction. Worse yet, Bjarne did it all of his own free will rather than being forced into that by "the committee" (which would've been a weak, but acceptable excuse).
If C++ were created just today, out of thin air and in its current form, there is no way in hell it would've seen any adoption. C++ is basically a long running bait-n-switch scheme. Come try a better C and then see it evolve into the Homer's car, which you have no choice but to use because it's now - surprise - an industry standard.
> Basically what I'm saying is, if functional programming is so great, why did LISP et al not take over the world? (Not a LISP hater, just curious).
Because the two aren't the same. Sure, some of the functional programming techniques were invented or had one of their first implementations in early Lisps. But Lisp family isn't built around functional programming per se (particularly with two of the most popular Lisps today, Common Lisp and Emacs Lisp, being multiparadigm languages, with the former having an OOP model that puts everything else to shame). Lisp did take off, but then died in the early 90s because of AI winter, and in the meantime C took off. Lisp never recovered, but it lives on in a way, having invented and refined half of the techniques we use in programming languages today.
(That's not to say Lisp is dead dead. Plenty of active development is going on. Scheme subfamily is alive and kicking, lots of code is being written in Emacs Lisp and Common Lisp too, and it shows up in many unexpected places. Hell, probably everyone at some point writes their half-baked, bug-ridden reimplementation of Lisp when they end up writing evaluators for XML or JSON.)
> So the big win with functional programming is easier testibility and fewer hazards when trying to multi-thread your code.
To give you my experience: during my phd, I developed https://ossia.io in C++. For the manuscript redaction, I rewrote all the core algorithms in pure functional OCaml. When I did some tests, performance was slower than -O0 C++ (so it's not even a given that multithreaded OCaml would outperform single-thread C++), the tests weren't meaningfully simpler to write, and it would be pretty much impossible to have an average comp. sci. student contribute to the code.
My experience multi-threading C++ code is, "slap cpp-taskflow, TBB, RaftLib" or any kind of threaded task system and enjoy arbitrary scaling. Hardly the pain it is made to be unless you have a need to go down to std::thread level, but even then using something like https://github.com/cameron314/concurrentqueue to communicate between threads makes things extremely painless.
It did, just people are so focused on parenthesis that they loose focus of the remaining parts of Lisp's ideas.
Guy Steele on Java: We were not out to win over the Lisp programmers; we were after the C++ programmers. We managed to drag a lot of them about halfway to Lisp.
This is indeed a very interesting question! I’ve also asked myself that time and time again, along the path of slowly discovering what this FP thing is all about over the course of my career. It does look wonderful, it does save you a lot of effort in some cases, where “some” is a vast swathes of computational problems. And its so damn elegant.
A lot of examples are thrown around were a bunch of lambda calculus can result in 10-20 fold reduction in source code size. So where’s the catch?There has to be something wrong right?
Well recently I stumbled upon a clojure conf talk that tried to tackle precisely this question. https://youtu.be/QyJZzq0v7Z4
The short of it is that its more of a historical inertia and chance than any specific hidden gotcha. Fascinating talk.
I'm sorry but this all strikes me as rather delusional.
The reason why functional languages doesn't take off is because programming languages are tools, not an end in themselves. Carpenters that are obsessed with perfecting their power tools would rightly be out of a job in a very short amount of time. Furthermore most of these "perfections" aren't even virtuous - in the sense of searching for improvement - but just self-indulgence and aesthetics.
Functional languages are dense, hard to read, have a high cognitive load, and are thus unproductive beyond the proverbial garage startup. They are also designed and used mostly by mentioned power tool decorators rather than practitioners, with the inevitable end result.
Totally understand the sentiment. I was very dismissive at first, as a self thought dev. Its really hard for me to learn something unless its helps directly to solve a concrete business problem I’m facing, so I know where you’re coming from.
I think my epiphany was when I was trying to learn me some clojure on a couple of weeks sabbatical from work.
As a mostly js/frontend/react dev at the time I was deep into React ecosystem for building SPAs. I knew and largely disliked the amount of code a redux based app would require in order to be “complete” with all the data fetching and state management (this was before hooks and contexts). And then I sew re-frame, that implements a lot of the features in that whole ecosystem, in like 100ish lines of code.
Thats BS I told myself. Things are missing! It cannot be just this code in front of me. But no, it was all there, just elegant and clear. All of the “meat” of the logic distilled to its essence, without any of the boilerplate.
It was just that I had to learn all the “higher level” stuff around FP, but it turns out those things are shared and repeated in lots of places, so once you index it in your brain, you can read the business logic itself, without all the additional “fat” of the code. Simply beautiful.
> As a mostly js/frontend/react dev at the time I was deep into React ecosystem for building SPAs.
Not to be too dismissive but I think that's the problem. Modern SPAs with React et al are very bloated and imo a massive wrong turn compared with the simple boring tech that was before it, with very little gain to show for it as well.
"Domain work is messy and demands a lot of complicated new knowledge that doesn’t seem to add to a computer scientist’s capabilities. Instead, the technical talent goes to work on elaborate frameworks, trying to solve domain problems with technology. Learning about and modeling the domain is left to others. Complexity in the heart of software has to be tackled head-on. To do otherwise is to risk irrelevance." - Eric Evans
Just some speculation, but one reason why C and friends took off better in enterprise domains is that they were on systems that people needed (C with Unix) or were more aggressively marketed. (Java) functional programming languages tended to not be as fast for single threaded programs compared to their imperative and object oriented counterparts.
Now that we have stopped seeing such large gains in single thread and performance, most of our scaling comes from being able to scale horizontally. Functional programming languages are making a resurgence: the rise of Rust, Elixir, Scala, etc. is in response to this.
Computing happened in three main waves: the big iron wave, the minicomputer wave and the microcomputer wave.
Each wave made computers cheaper and more affordable, but also, initially, substantially less powerful. At each wave, operating systems, languages and other software from the previous wave was not able to make the jump over. For instance, Multics didn't make it onto minicomputers, but some fellows who had worked with Multics created something scaled down to fit called, jokingly, Unix.
Each wave opened up computing to more people, and most of those people were forever ignorant of the software that could only run on machines of the past, or institutional machines they couldn't personally afford.
Lisp, being one of the oldest languages, belonged squarely to the first wave. It started small, but grew along with institutional hardware. By the time microcomputers came around, modern Lisp wouldn't fit. It took at least 15 years from the time computers became available and affordable to ordinary consumers to the time a fully featured Lisp would run on a computer affordable to the ordinary consumer.
A whole generation of hackers had no experience with Lisp at all. Senior programmers who are now in their 50's might have started coding as kids on 8 bit micros, using BASIC (which was useless for non-toy applications and games) and assembler. Anyone younger than those people would be likewise Lisp ignorant. The result is that we have entire organizations, from very senior people on down, who don't know anything about any Lisp.
There was some Lisp action on micros, but attempts to fit Lisp into those environments only harmed LIsp's reputation more than anything: the whole "interpreted, memory-hungry and slow" stereotype.
One language which made the jump from minicomputers to micros was C. In the middle 1980s, micros were becoming about as powerful as 1970's minis, which played out very well for a language designed for efficiently programming 1970's minis. C had to make only a one-generation jump.
There were companies selling Lisp machines in the 1980's, but these were expensive minicomputers. In this intervening time, minicomputers had become a lot more powerful, and so were able to run Lisp well. But the world had long moved on to microcomputers being the hot thing. Programmers were licking their lips at the prospects of making money in a large, consumer market. That included some Lisp programmers. Some of them hit a stark reality: they had to rewrite their code in something else, like C, to get it to run well on micros. A good case-in-point is the CLIPS system (https://en.wikipedia.org/wiki/CLIPS). CLIPS is essentially a C clone of an earlier system called OPS5, that was written in Lisp. That is why CLIPS uses he Lisp-based surface syntax. That OPS5 was also rewritten away from Lisp, using Bliss.
Lisp showed a resurgence in the 1990's, and one factor we have to thank for that is that consumer machines finally started showing memories measured in double digit megabytes and clock rates pushing past 100 MHz.
Now, note how all the modern languages that borrow from Lisp are incredibly memory hungry. Javascript, Python, you name it; those things could not have ran on anything affordable in 1990. Not in their current form and the way they are used.
In summary, there has been a recurring history in computing in several waves, each one representing a big regression in hardware capabilities, accompanied by a big jump in affordability. Each wave produced its own systems and languages due to the influx of new people not familiar with the older systems, and due to the older systems having specifications that would not fit. In each wave, there were some developments similar to what had occurred in the previous wave, but in new forms.
CLIPS was not a direct OPS5 replacement. CLIPS was replacing the usage of ART (AUTOMATED REASONING TOOL) at NASA. ART was a proprietary, large and extremely expensive expert system development tool written in Lisp, which required workstation class systems. ART had a rule system influenced by OPS5.
There were/are also cheaper versions of OPS5 itself written in Lisp.
Programming, and functional programming in particular, has changed a _lot_ in C++ since 2012:
* Important libraries have been developed (e.g. Niebler's ranges)
* The community has gotten used to C++11 constructs and getting the most out of them
* The language itself has evolved, with 3 new standard revisions in 2014, 2017 and 2020; the latter change is actually pretty significant and we're only starting to observe its effect.
So, even before reading this piece, you must realize it is quite outdated.
Is that what he says? I thought he said “As functional as makes sense, but not more.” I haven’t read this post in a while, but it’s definitely a worthwhile read.
Your interpretation is correct. He describes things that you shouldn't try to imitate in C++ (like appending an element to a vector by creating a copy and then appending to that). But also describes other situations where having a pure (or near-pure) functional version can be greatly beneficial. Like a method on vectors (math sense) that return a normalized version, or adding string methods which return a copy rather than mutate in place (think: upcased versus upcase).
Yes, that's what he also says, but this works best for very simple parameters. If things get interesting and you have bigger data structures there is (too much) performance penalty. I am a bit cherry picking here, but let me just cite "Actual functional languages are implemented in ways that make this not as disastrous as it sounds, but if you do this with typical C++ containers you will die."
He's specifically referring to one thing that shouldn't be done in a functional style in C++, not saying "you can't really do that" as you originally post. The rest of the article is advocating a move towards pure-r functions in C++ (at least in addition to impure, mutating functions).
Maybe I come from a different direction, lots of data. If you do any functional programming on these you are creating new lists/vectors (the type) left and right. To me this is the gist of functional programming. So when I started the article, I was wondering how would you do that in c++, well his answer is you shouldn't. So when you are saying it is just one thing for me it is like a car without an engine (also only missing one thing). (the other thing about functional programming is the widespread use of recursion, but this is not even mentioned in the article)
“The lack of any language support in C++ for maintaining purity is not ideal.”
I get that C++ is already bloated and bolted on to C as is but I would have preferred to see changes like these instead of sugary bloat like “auto”. Adding a “pure” keyword allows testability not otherwise attainable without modifying the compiler. Adding “auto” is just a me-too to the “var” of C# and Java.
Basically, I’m trying to say that C++ needs to have its standards committee fired.
> Adding “auto” is just a me-too to the “var” of C# and Java.
I get the "C++ is so full of nonsense, and they keep adding more of it!" attitude, and I largely agree with it, but "auto" is a bad target. auto makes a lot of c++ code (most, arguably) much easier to read and cleans up and simplifies a lot of the language. Like, the fact that you never, ever, have to write out the full name of iterator types in C++ (you can just go "auto it = vec.begin()" or whatever) is worth the feature all on its own. It also makes dealing with complex generics simpler. It's a huge quality of life improvement.
There's a reason most languages have added type inference, and it's not just a trendy bandwagon. In most reasonably recent languages (Rust and Swift notably), it is in fact the default to do this kind of type inference. It's genuinely a nicer way to program.
In C++ it also serves another important purpose: if there wasn't auto, there wouldn't be lambdas. All lambdas in C++ have an type created by the compiler which you can't access the name of. In order to store a lambda in its native type, you therefore have to use auto. Basically, if you like that C++ is becoming "more functional", that is only really possible because of adding auto.
Agreed. Another way to think about it is the language is trying to get the compiler to do most of the heavy lifting. Stuff like auto makes sense with this attitude because why have the programmer think about what the type should be when it can be infered by the compiler? Also see templating.
I've come back to C++ after being away from it for a few years, and modern C++ (17, 20) is a delight to work with compared to what we had in the 90s. RAII especially...
It's been proposed already. One of the issues with the proposal that jumps out to me is that it wants memory allocation to be not allowed in a pure function. Which is both fair & not. That means no collections, more or less, as you can't allocate the return value.
But if you allow memory allocations, then you go down the rabbit hole of well whose memory allocations are allowed? Only `new`? What about `malloc`? Or a custom allocator? If custom allocators are allowed, do they need a `mutable` keyword equivalent but for pure functions, where it's internally not-pure but is allowed to be used from a pure function?
But it looks like there was no follow-up proposal. `constexpr` isn't too far off from a [[pure]] specifier, though.
I can say this is not true for the general case. C++ has - against the odds, in my opinion - been dragged kicking and screaming into the 21st century. It is not exactly design by committee, because there are a lot of (semi)independent developers driving the design choices. The choice of features for a given release is by committee, however, and reflects a wide variety of opinions and interests. Not the least of which are the people actually providing compiler support. Functional programming in C++ is still not always facilitated by the standard library, but it's miles beyond where it was a decade ago: moving in the right direction.
I agree on the merits of "pure", but I wouldn't call "auto" bloat. As a quality-of-life improvement, especially for writing generic code, it's pretty useful.
Because there's no ecosystem of pure functions, a single impure function in the transitive closure of your dependencies can ruin your day. (Unless you depend on no code written by other people.)
Compare with haskell, where effects are enumerated and purity is the default: most functions are pure.
> Unless you depend on no code written by other people.
Yeah that is me. I can certainly see how a program that uses a lot of libraries would have issues with pure though. Not sure what the solution to that would be other than going full haskell mode, which isn't what people who use C++/D would want anyways
Idiomatic D code tends to use a lot of templates so this is true but much more prevalent if you write C-with-pure than (say) the code I tend to write (an incomplete C preprocessor+binding generator at compile time is the most D-ish thing I've done yet)
I honestly don’t get the rage with functional programming.
I find the hardest parts is GUI and user interactions, this is a place where storing state is natural.
Perhaps more on the server side functional programming is useful.
Can somebody meaningfully explain to me why there is such a swell of interest in this field? Is it just all the ML and data scientists buzzing about an approach that is naturally suited to them or is there broader implications? I wouldn’t have guessed that everybody on HN was working on optimizing multi threaded applications or any of the other valid reasons to consider FP more generally.
I think about this quote often in my day to day programming. Object oriented programming is so full of stateful objects and it's easy for programmers to add listeners to all sorts of events. Even on the cloud side now you have all these micro services and trigger functions and small changes can have cascading consequences.