170 comments

[ 3.9 ms ] story [ 229 ms ] thread
Functional programming with immutable state cannot possibly win in the general case.

There are two truths that ensure the dominance of imperative software:

1. At some level of software complexity, programmers MUST start to organize data into composite objects. They have to do this because working outside of well-defined problem domains is a recipe for buggy software and spaghetti code.

2. Copying memory around to enable to facilitate these immutable structures is SLOW.

Are you considering the speed of persistent data structures, that have the same big-O as their mutable counterparts? [0]

0. https://en.wikipedia.org/wiki/Persistent_data_structure

Algorithmic complexity isn't the same as speed.
True. My use of the word "speed" is incorrect, or at least imprecise.

The parent comment claimed that immutable data structures are slow, because you would have to copy them around, an O(N) operation.

I wanted to know if they considered that operations on immutable structures can be done without copying, and have the same algorithmic complexity as the mutable data structures, if they used persistent data structures.

> 2. Copying memory ...

the entire point of some of the immutable structures is hidden in their implementations, such that they are fast exactly because they do NOT copy unnecessarily. the side effect gets called "functional", so there's a strange reversal of importance of (kinda misleading) buzzwords at play here.

The link(s) the others shared is a great article.

Ah, but you don't always have to copy memory around to facilitate immutable datastructures. Koka lang and Roc lang are at the forefront of a functional-but-in-place (FBIP) style of memory management that use hyper-fast ref-counting and if a function owns the sole reference to a datastructure can mutably modify it instead of deallocating & reallocating. The most recent Perceus reference counting paper, implemented in Koka, had Koka's functional and persistent Red-Black tree 10% faster than the handwritten C++ version (std::map) [0], and the Roc language has a purely functional quicksort version competitive with standard imperative languages.

So it seems we can have out cake and eat it too!

[0] - https://www.microsoft.com/en-us/research/uploads/prod/2021/1...

Does Perceus differ substantially from using https://lib.rs/crates/im and writing code which looks identical to imperative code, but cloning is faster and mutations are slower (and copy internal structures as necessary)?
It does the same mutate-if-unique optimization as im, but it does so automatically and more broadly as a part of its optimized reference counting. Perceus can go beyond what im can do by re-using allocations between static types - one of their examples involves doing a tree traversal with O(1) extra space by using a zipper which gets optimized by Perceus into a Morris traversal - and the programmer doesn't have to do anything special to get the benefit! (of course, writing code in such a way that you know it will be optimized has performance benefits if you do know how it works) This sets up this wonderful situation where you can write easier-to-maintain and easier-to-verify-correct algorithms that get optimized into very efficient versions that would be hard to write by hand even in a mutable/imperative language.

Their papers and online docs are very good, I highly recommend them for more information!

> Copying memory around to enable to facilitate these immutable structures is SLOW.

"Optics". No one writing serious FP code copies memory around willy-nilly.

No, but the compiler may do it for you.
why it would copy and not be passing immutable data by reference?
> 1. At some level of software complexity, programmers MUST start to organize data into composite objects. They have to do this because working outside of well-defined problem domains is a recipe for buggy software and spaghetti code.

Right. and FP provides NO way to composite objects

> 2. Copying memory around to enable to facilitate these immutable structures is SLOW.

Who says you have to (multiple others have pointed to resources)

FP maps perfectly to networked applications that integrate more than one machine (i.e. client/server, APIs, and larger distributed systems). networked applications arguably IS the “general case”; it’s the platform specific stuff which is getting paved over by data/function abstractions
The biggest problem I have with functional programming is debugging.

On imperative code, execution is easy to follow. Instructions go one after the others, loops loop, function calls and local variables go on the stack, globals are always visible. Optimization aside, what you code is what the computer runs. You often have a debugger to step through your code, or some logging facilities, even if it is just a printf.

On functional programs, the idea is usually that you manipulate stuff, but you don't know what you manipulate. You apply a functions to functions making other functions until you end up with the function that turns the input of your program into the output of your program. Then you let the compiler do its magic, it is actually really good at that and not slow. But then you end up with something that doesn't look at all like what your wrote (computers are imperative), and if, in the end, the result is wrong, good luck finding where. The debugger will give you the state of the program in a program that has no state, and logging is a side effect in a program without side effects.

Functional programming is not bad, I love pure functions, but in the end, I think it is just a tool. A solution to a set of problems, but not enough to stand by its own for most projects. And it is evident from most modern programming languages. Most of them have some functional paradigms, and could likely be used to write pure functional code, but is better suited as something to use just when you need it.

I don't think this is accurate at all.

Debugging in a strict FP language is pretty much exactly like debugging in an imperative language. Debugging in a lazy FP language requires somewhat different approaches, but still does not have the problems you are implying here.

> In the end, the result is wrong, good luck finding where.

This is not true. If this was true, nobody would be able to build significant things in FP languages.

> Optimization aside, what you code is what the computer runs.

> But then you end up with something that doesn't look at all like what your wrote.

No, you can trace code down through the compiler transformations and generally understand what is going on at each level of abstraction.

Compilers for imperative languages are just as "magical".

I like FP but debuggers fail with FP because debuggers are imperative tools. There's a definite miss match in both UI and concept. It's not the fault of FP itself, it's more the fault that we haven't come up with such an interface yet.

If you want an FP debugger the interface has to be able to step by step evaluate an expression. Since FP is just a giant expression, how is it evaluated step by step? Break points will be different and stepping through it will be different as well.

Take for example:

    (1 + 2) * 3 + 2
Stepping through it with a debugger would look like this (imagine the expression transforming at each step):

    (1 + 2) * 3 + 2
    (3) * 3 + 2
    9 + 2
    11
A break point would look like this (square brackets indicate a GUI marker)

    (1 + 2) + [(4 * 6)] + 1
A run until breakpoint will evaluate to this:

     (3) + [(4 * 6)] + 1
That's it. Debugging FP is about stepping through expressions. Debugging Imperative programming is about stepping through statements. And the unique thing about "stepping" through an expression is that the expression is getting reduced (aka changing) at every step.

You need a complete UI overhaul for FP to work. This tends to be harder in terms of building a UI, because text by nature is a one to one mapping to procedural instructions as text Lines are equivalent to instructions. However for FP everything can basically be on one line, so text doesn't really fit and thus you actually need a more dynamic UI for a proper FP debugger.

This isn't my experience at all. Imperative code with side effects is harrowing to debug because it may do different things depending on what execution path got you there.

Pure functions are easy to debug. Run it, check the result against expectations. Step in if necessary.

I think this comes down to experience. My favorite programming language is Agda and I have a lot experience writing code in Haskell and Lisp variants. I personally also find it trickier to debug in functional languages. I believe this comes down to how you debug: I am comfortable using a debugger for debugging but I simply find it easier to debug using prints (for context switching reasons) but this is not always an option in a purely functional context without any IO. This does push me out of my comfort zone sometimes, and I would say it's accurate that I debug much faster in imperative languages. Even though I write better and safer code in functional languages (and even though I like them better).
> On imperative code, execution is easy to follow. Instructions go one after the others, loops loop, function calls and local variables go on the stack, globals are always visible. Optimization aside, what you code is what the computer runs. You often have a debugger to step through your code, or some logging facilities, even if it is just a printf.

I think you're doing something wrong.

Because your experience should be exactly the opposite. Imperative programming by distributing its state can only really be understood in context. You need to understand that instruction one after another, just like you said. You need to understand globals and the overall state.

In functional programming you understand code without context. Everything is local. There's nothing to trace.

> Then you let the compiler do its magic, it is actually really good at that and not slow. But then you end up with something that doesn't look at all like what your wrote (computers are imperative), and if, in the end, the result is wrong, good luck finding where

What does the compiler have to do with anything here?

You can print out whatever state you want, your debugger can provide you with whatever local state you want. There's nothing obscure or mysterious about functional code compared to imperative code. It's just a matter of not distributing your state widely.

> In functional programming you understand code without context. Everything is local. There's nothing to trace.

The problem is people don't trust it's really local even though it is.

>I love pure functions, but in the end, I think it is just a tool. A solution to a set of problems,

I'm tired of seeing this analogy everywhere. Everyone knows it, there's nothing new here. When we talk about things like FP or OOP, the real opinions live at the extremes. Where one tool is definitively better than another tool or where some tools are complete garbage.

Saying that everything is a tool for different things or it's all apples and oranges says nothing about anything.

Functional programming doesn't actually compete with imperative programming.

In practice, functional programming is a way of organizing imperative programs. Kind of like how OO is a way of organizing imperative programs.

Almost all applications written in Haskell use the IO monad quite a bit, for example. Large Haskell projects generally use a lot of C libraries, and sometimes even directly include C code for performance sensitive bits.

I think part of the problem is no implementation has an exciting story about writing the "shell" part in a different language. I love love love writing purely functional code. Probably my favorite thing to do. But what a lot people who also like functional programming are missing is writing imperative code in functional languages almost invariably sucks. At the end of the day we all have to write the "shell" part of our Haskell programs, and my programs always end up in a horrific mess of IO, State and other monads.
> writing imperative code in functional languages almost invariably sucks.

I don't agree at all!

Haskell is extremely pleasant for imperative programming. There's a learning curve for sure, but you get a lot in return. (STM is one small example). I would much rather write imperative Haskell than Python.

It does gets messy when you start writing really low level code (direct pointer manipulation, etc). And it sucks for small scripts (too much project boilerplate, small standard library). But those are the only real pain-points.

> At the end of the day we all have to write the "shell" part of our Haskell programs, and my programs always end up in a horrific mess of IO, State and other monads.

You can very successfully keep nearly all IO at the edges and do the heavy lifting in pure functions if you make it a goal.

> At some level of software complexity, programmers MUST start to organize data into composite objects.

I don't understand this point. All functional programming languages have algebraic data types and records, which are composite types. If this is not a "composite object", you'll have to clarify what you mean.

> Copying memory around to enable to facilitate these immutable structures is SLOW.

Slower than what and in what context? Append-only logs in relational databases are immutable data structures, and they enable multiversion concurrency control, which is faster and more scalable than locking when there's lots of contention.

Unqualified claims like "immutable data structures are slow" is just wrong.

Why have immutable data structures not taken over in the general case if it's a strict improvement? Sometimes they're good and sometimes there not. The issue is functional programming needs them to be used in every case.

Personally I'm of the opinion that purity is impossible so if a paradigm needs purity it's going to be an uphill battle.

FP doesn’t require their use in every case. For example, Clojure allows mutation as an optimization. But for the normal path, immutable data structures are just fine and help avoid a whole host of bugs.
I'd argue that means Clojure lets you use paradigms other than FP. FP is still FP.
Note: the log (aka write-ahead-log) in Postgres, and its ilk, enables the D(urability) of ACID. MVCC in Postgres is implemented by storing the visibility info of each row in the row-header (search: xmin, xmax).

Some commercial RDBMSs do use their logs for MVCC, though.

> I don't understand this point. All functional programming languages have algebraic data types and records, which are composite types. If this is not a "composite object", you'll have to clarify what you mean.

I think they meant data structures, not data types. Things like lists of records that each contain further records.

Data types like records and algebraic types are data structures. Here's an algebraic type for polymorphic lists:

    type 'a list = Nil | Cons of 'a
You and the other person don't understand.

All data structures have types. EVEN lists of records that each contain further records. A type or a data structure is an orthogonal concept to FP, because types exist in imperative programming and data structures also exist in FP.

Others already argued against your point, but I want to point out that (2) is simply wrong. You can have mutability in functional programming -- not in general, but there are cases where this can be expressed. For example, in a very simple example:

   a = [1, 2, 3]
   b = a.append(4) // append: list->int->list
   // Rest of the code never refers to a, but refers to b
In this code, you can prove that it's safe to move a's memory into b and thus append operation can be done without copying a. I'm not implying this is easy to implement, but it's clear that this is not a fundamental issue in functional programming, rather a deficiency in its current implementations.
Right, but FP has been around for quite some time, and given that none of the current implementations actually solve this issue right now I think it is a valid critique of FP because it's simple what current FP implementations are limited to.

Maybe a "sufficiently advanced compiler" in the future will solve this, but it's not at all clear such a thing can be created in a way that's practical right now.

> ...and given that none of the current implementations actually solve this issue right now...

This has been possible in Haskell for a long, long time. A ton of languages do something very similar with strings (Python, JavaScript, etc.) Strings are immutable in both Python and JavaScript, but rather than using a "StringBuilder" class like in C# or Java, the compiler and runtime will optimize certain pure functional operations (like string concatenation) into a mutable operation on a buffer. If you are not aware of this optimization, you might find yourself benchmarking some simple string operations and be completely shocked at how fast they are. (There are a couple questions on Stack Overflow where people are confused about why their Python code is faster than their C++ code when benchmarked.)

I think the term "sufficiently advanced compiler" does a bit of a disservice. People have used that phrase a lot, not only when talking about high-level languages but also when talking about things like ISAs. It turns out that people are bad at predicting what kinds of optimizations compilers will be able to do in the future.

The other reason I don't like the phrase "sufficiently advanced compiler" is because these optimizations are often tied to something other than the compiler. They may come from advances in library implementations, advances in programming languages, or advances in language runtimes.

Take a look at how Haskell does it. You have array implementations with a pure interface but impure implementation. You have a system called "stream fusion" which lets the compiler take array operations that logically produce arrays as values, and transform these into simple operations that loop over the array. Stream fusion is a feature that doesn't really require a "sufficiently advanced compiler", but rather some relatively simple compiler features (all things considered, "simple" is relative) and the ability to annotate library functions with transformations that improve the generated code at the library functions' call sites.

I think of stream fusion as more of a "let's write good libraries" feature, and less of a "compiler magic" feature.

Then take a look at how Python does it. When you concatenate strings, the library code can check that the left-hand argument has no other references, and modify it in-place. All the compiler has to do is make sure that variables are killed as soon as possible, which is a very ordinary thing for compilers to do. It's not magic, there are ways to break this optimization from happening, but it does work.

> Right, but FP has been around for quite some time, and given that none of the current implementations actually solve this issue right now I think it is a valid critique of FP because it's simple what current FP implementations are limited to.

No idea what you mean.

Mutable state is not a problem in Haskell. You can have high performance libraries that provide mutable APIs that are safe to use. You can have code that looks more imperative if you want. If you really want to go nuts you can use linear types and solve such problems in a completely general way.

> Maybe a "sufficiently advanced compiler" in the future will solve this, but it's not at all clear such a thing can be created in a way that's practical right now.

There's no need for any advanced compiler. We have all of this today.

This is a really uninformed opinion. I'm disappointed that it's currently the top comment.

You really think functional programmers don't build composite types?

It's like listening to a flat earth advocate.
Have you seen the AWS go sdk? It basically builds linked lists (no locality) of everything and marshals to and from json over and over like it's free. I mean there's no thought whatsoever to making efficient use of memory, getting locality for improved performance, etc.

And if you think about it, most things using the aws sdk are network i/o bound anyway, which is probably why they burn ram and generate garbage like it's free.

So immutable data structures (especially persistent ones) aren't a problem at all in many domains.

Well written compilers for functional programming language do not copy immutable data. I recommend searching for “Immutable Data Structures” to see how you can implement very high performance immutable data structures. Haskell and Closure (for example) does this and Haskell is faster than most imperative languages out there. I recommend learning more about this subject.
This comment is a fine example of what it's like to have strong opinions about something you've never used.
Immutable data structures often don’t copy a lot of data around. Immutable linked lists have tail references from each head. If something falls out of scope it gets collected by the garbage collector. Also if you don’t share memory between processes, you can free that memory once a process terminates, which may seem orthogonal but it’s a core principle in Erlang.
Those copy-on-write linked data structures, with the implied pointer chasing, have a cost. That's what the parent is talking about...
A lot of languages, like Erlang/BEAM, use pointers under the hood and aren't necessarily copying anything.
2. Copying memory around to enable to facilitate these immutable structures is SLOW.

This is categorically wrong. You actually don't know what you're talking about.

In an functional programming language, because everything is immutable, the compiler just MOVES everything around. There is ZERO copying in a functional programming language. In fact there's no explicit command for it either. There is zero reason for you to copy a variable that is immutable so there's no copy() function.

What you're referring to is more of what happens when someone is writing functional code in a language that's not functional.

> What you're referring to is more of what happens when someone is writing functional code in a language that's not functional.

And even then, there is the question of whethwr it is a deep or shallow copy on receiving or write.

just pass a const reference. It's immutable anyway.
Unless that ref holds a mutable reference. C++ doesnt make it easy at all to have something be truly immutable.
Copying is an implementation detail, it doesn’t have much to do with FP in itself. If anything, due to the compiler knowing that variables can’t change, it can optimize away copies much much more aggressively than a “traditional” PL.
Side note, GitHub's URL scheme is so counter-intuitive.

I spent the first 10s wondering who this readme user was and how could they have such a different main page.

Then there's the orgs at the same level as users...

I guess the mods here need to create an exception for such paths, and simply label them as 'github.com', rather than the usual scheme of showing the user/org name.
Agreed. I'd have preferred a top-level discriminant, like Reddit has with /r and /u paths at the top.
I thought it was already mainstream based on features of long popular languages. Features that debuted about a decade ago.
Most of these languages are functional-ish, meaning they have some capabilities derived from purely functional languages, but they don't have all of the features or power.
Yes, that’s my point, and they’ve been released for years now. I don’t see Haskell, Clojure, or even Lisp going mainstream even today.
Great I am looking forward to seeing mode code like this:

  timeslotRepo.findAll().stream().filter(timeslot->timeslot.getEndDate()==null).first().ifPresentOrElse(()->{}, ()->{new Timeslot(DateTime.now())})
Well, that's an ignorant reduction of functional programming but suit yourself.
It's exactly the experiences I've had trying to read functional-style Rust code that uses higher-order function methods as a replacement for control flow primitives.
Yes, it’s far too readable to be actual functional programming.
I only write code in an FP language and it never looks like that. Usually I’m passing function results into another function or into a match to break out success and failure states. It doesn’t have to be Haskell wizardry, or even clever.
That's a slap in the face to Erlang/Elixir or maybe you weren't able to comprehend it?
Readability and the ability to comprehend code with poor readability aren’t really related.

I was having a bit of fun at the expense of FP zealots.

So you're filtering specifically for timeslots where the end date is null and therefore guaranteeing that you return a timeslot dated to now?

  if timeslots.stream().matchNone(Timeslot::hasntEnded) {
    new Timeslot()
  }
Would be an improvement I guess. The way I found this code it was also wrapped in a @Transaction so it creates a new object in the database with spring magic. There was a lot of other things wrong with the code, that was not my point though. Lots of people just think FP better and of course in theory they are right but most people don't think for themselfs.

People are quick to act insulted whenever I give some critical remarks about FP.

When would you use a forEach function instead of a for each statement, I suggest maybe only use the function if the callback is actually a higher order function you get from somewhere else as I don't see the point of wrapping your logic in a closure.

Same thing with the use of observables/reactivity in UI programming, again it is illegal to question this orthodoxy. But then you take a look at how graphics programmers use immediate mode programming and fancy compute shaders to draw the entire frame every frame it makes me smile at how much simpler everything becomes.

I'm generally a proponent of OO, and I actually think this kind of filtering chain is one of a number of places where fp code can be more readable than imperative loops.
It's also, more or less, the "fluent interface" style if laid out differently, other than the lambdas (but even those are in both C# and Java):

  timeslotRepo.findAll()
    .stream()
    .filter(timeslot->timeslot.getEndDate()==null)
    .first()
    .ifPresentOrElse(()->{}, ()->{new Timeslot(DateTime.now())})
https://en.wikipedia.org/wiki/Fluent_interface#Java
If you used line breaks I don’t see how thats any less readable than a block of procedural code.
If you added line breaks I wouldn't be sure if you were arguing for or against functional programming.
I'm not sure how I feel about this.

If you look at my code in "multi-paradigm" languages like typescript, there's not a heck of a lot of loops, mutations or side effects. I like expressions, and immutability, and higher order functions. They make things shorter and means there's less state for me to keep track of - or screw up.

But at the same time, I like objects. Almost all of mine are immutable. I went through a phase of using plain old data + a collection of functions that transform them, but found a good object where all of that was self contained simplified things a lot.

I also have to say that sometimes FP programmers don't seem to understand OOP a lot. I'm of the opinion that working on an old school opinion that a legacy Java codebase doesn't really make you an expert on OO.

So while I'm glad we're thinking about purity, and avoiding side effects, I'm kind of sad that there's an object backlash. They're fantastic contstructs.

With immutable objects, aren't you just left with syntactic sugar around structs and functions? I'm picturing CLOS, which is undoubtedly an OO system but lacks 100% of what you use objects for. Are there languages which let you use dot-syntax for "methods" which just wrap normal functions?
What does CLOS lack that you want from an OO system?
Nothing! I mean I personally just use defstruct unless I want inheritance, but that's certainly not because CLOS lacks anything. I meant "you" as in the person I responded to, who wants immutable data structures with tightly coupled functions. It's because CLOS is among the most object oriented systems I know of that I used it as an example; there seem to be some basically orthogonal traits which nonetheless wind up highly correlated, and CLOS is the purest OO you can get this side of Smalltalk while looking different from what you see in Java or whatever.
With immutable objects, aren't you just left with syntactic sugar around structs and functions?

Yeah I've gone down this thought experiment. Conclusion I came to is that while you could view it that way, there's something powerful about Just Having One Thing, not structs + functions. It's almost a mindset shift as well, like you're requesting the object to do something and don't care how.

Are there languages which let you use dot-syntax for "methods" which just wrap normal functions?

Yeah, Dlang has that.

https://tour.dlang.org/tour/en/gems/uniform-function-call-sy...

> It's almost a mindset shift as well, like you're requesting the object to do something and don't care how.

I definitely see what you mean, a lot of FP encourages very general abstractions rather than something domain specific. That being said, I think Elixir has a pretty good handle on using functions as sort of black-box interfaces you can change later. Not sure if that's a language thing or a culture thing though.

> Yeah, Dlang has that.

Ooo, that's really nice. Super simple but a definite improvement to readability. Reminds me of |> in Elixir (or ~> in Clojure), particularly the chaining part. It clicks differently though, and definitely makes things readable for many programmers. Now that I'm writing this out, doesn't Rust have something like this? Like functions strapped right to a struct?

I suggested this wonderful feature to julia but it was rejected:(

Glad to hear D has it.

as OP said it would make functional languages more ergonomic (especially for OO folks) and most importantly enable code completion!

There already is a syntax for that: a |> bar |> foo
neat, thanks!

it's still a bit unergonomic to type but a very useful beginning!

Reminds me of hurting my fingers in c where you had to type '->' instead of '.' !

At that point you're left with two benefits: 1) Organizing your codebase so that it is easy to find which functions operate on which data 2) Controlling access to those methods on the programming language level. I.E, TankerService is the only class with Tanker instances and therefore the only part of the code with access to those methods.
Dynamic dispatch is really what differentiates OO from just being syntactic sugar around structures and functions.
The problem with state in objects is that in more complex systems you often need to change a bunch of related state at one time. When it's all bound up in individual objects that manage their own state this becomes very complicated.
Sorry, I don't get what how that's harder.

    newData = f(oldData)
VS

    newObject = oldObject.f()
Furthermore, it chains nicely and offers pleasant autocompletion in your IDE:

    newObject = oldObject.f().g().h().i().j().k()
VS

    newData = k(j(i(h(g(f(oldData))))))
Just to steel man the "Functions and Data" side of the equation in a lot of FP languages that'd be something like

    newData = oldData |> f |> g |> h |> i |> j |> k
And to straw man the OO way a little...

newData = oldData.f().g().h().i().j().k()

wont work by default unless you also happen to return the object after every method (builder style) which looks weird and is not standard

I don't know what you mean. You can return a different type, the only constraint is that the methods don't return void and the subsequent method exists. This has nothing to do with the builder pattern.
You still have to do work though, to ensure, that always some fitting object with the correct methods is returned, so that you can chain further. Functions by definition (as opposed to procedures) return something and as long as that is the correct thing or type of thing for the next function, all is fine.
You don't have to do work; at any step you have the functions available that operate on the object returned (whatever it is). On the "Functions and Data" side you are similarly constrained by the possible functions that operate on that data.
What I meant by "work" is, that inside your methods you have to return an object, which has the method, which is the next thing in your a().b().c()... chain. Often in OOP you would not return the object itself, unless you do the builder pattern. Of course you can return other objects, if it is intended, that the chain has a call to a method of another object next. However, you may only call methods of the returned objects and you must return objects.

In the function and data scenario, all that needs to be made sure is, that your functions have the return type, which the next function expects, otherwise it is a type error, regardless of whether the language complains or not. A function call chain implicitly assumes the types to be correct, otherwise you will likely run into an error.

The difference is, that in the functions and data scenario, you already have functions, which take data of some type and return data of some type. You do not change them in any way, while on the other hand with objects and methods you need to change the methods, so that they do return some fitting objects.

There's no law stopping you returning a new object from a method outside the builder pattern.

Immutable objects are a pretty old idea, smalltalk had select (map) methods that returned a whole new collection back in the 80s.

...and in fact most strings in OOP languages work like this:

    String next = original.substring(2);
this has nothing to do with OO, function chaining depends on the output of the previous function regardless
Chaining is one way of making function composition more ergonomic, but there are others. Functional languages tend to have an operator (e.g. |> in Elixir. Think x |> f() |> g() |> h() |> i(), etc.) that gives the same benefits of chaining, but the functions don't have to be methods providing greater flexibility.
You can have the chaining syntax without object-orientation, like in Nim, D, Koka, VimScript, …
Right that's one object. Often you need to change the state of a bunch of related objects in a transactional way. The way redux works for example. It's somewhat analogous to database transactions.
Not sure what you mean here. Redux changes the state of one piece of data - the thing inside the redux store. Really not hard to imagine that with one big object in the exact same fashion.
I’m not a functional programmer by trade, but I think you’re alluding to (and should reframe your argument to discuss) concurrent reads and the implications of immutable data in a multithreaded scenario. It’s about simplifying your programs state machine. If I have some object A in an OO world, some N threads may act on said object concurrently resulting in some degree of non-determinism for any function of A. In FP, as I understand it, this is mitigated as you’re not dealing with mutable data structures but immutable objects whose data is consistent over the lifetime of any logical operation. Effectively, you just need to deal with conflicts on writes which you can design for.

This is just a side effect of functional programming but such behavior can be embedded into and made a fundamental part of your foundational data structures in an OO program.

Can I make a stupid example? Lets assume you got some building with people inside. One person exits the building. mybuilding.leave(person) or so. What does .leave(...) do? Ah, it needs to reduce the counter of people in the building by 1. Oh and by convention this means, that the person is checking out, so lets update their schedule to account for that. Ah they are also currently "out of office". Ah and the person at the door, who keeps watch has now seen another person leaving. Maybe the person went through a scanner. The scanner statistics need to be updated.

And so on and on. All kinds of objects need updates for their states. You need to direct all this stuff, without forgetting things. Otherwise the state of your objects will become inconsistent and at some point it will show as some kind of bug. At that point you might find yourself debugging the whole thing, to find out when the inconsistency starts or what causes it.

No need for multiple threads or any of the kind. Complications from splitting the state up and parking it in various objects and having to tell the computer how to update these objects. Instead of looking at 1 function at a time, which only works on its inputs and gives an output, you have to remember to call all the appropriate methods across some reasonable part of your entity landscape.

If you have a bunch of related objects that need to change together in a particular way, encapsulate them in a bigger object and offer methods that return the new, different bigger object.
Depends on the problem. Sometimes the locality of every state inside the function you write is beneficial, other times it makes it much easier to just not care about the state of a given thing, and let the encapsulation do its job. I just want to use it as per its contract.
This is the year of Lisp on the desktop! Or was that Linux goes functional programming? Haskell in the database? Immutable documentdb goes webscale?

Javascript was an ugly thing on the browser, I do hope that webassembly helps with that, but I highly suspect that webassembly will be for evil programming that is even harder to decipher in compiled-bytecode format.

Alas, the article is riddled with many of the same problems that FP evangelism is usually saddled with, but this time with slightly more colorful fonts.

1) do not say or show the symbol for "lambda".

2) immutable: such a harsh, harsh word.

3) Yup, there's the word "pure". Heil, comrade, happy sharia to you.

4) AHHHH, "higher order". Expanding the consciousness. Take a puff, man.

Well, at least he didn't say anything about monads, functors, or category theory. And there's no implementation of the fibonacci sequence.

Really insightful post, specially if you share with us what is it you've been smoking.
IMHO Scala did more for practical FP than any other PL except for maybe JS as it is basically bastardized Lisp.
I do not think it is reasonable to call JS a Functional programming language.
No but it popularized practical functional programming through react.
React functional components have nothing to do with functional programming. Hooks are stored in global variables.
Is Erlang functional programming? Anything in an Ets table is a global variable. There's even mutable state hiding in the process dictionary! You might have to access a database, which is global state! The horrors!
In Joe Armstrong's own words: Erlang is either the most object-oriented or the least object-oriented language.

It also very much discourages mutation and favors pattern matching and pure functions, so I would say it is quite FP in nature.

And Haskell compiles down to C- as a step, which is as imperative as it gets. I don’t see how an implementation detail determines the degree of FP.
React does have little to do with it, I would say. Its components are quite the opposite of FP actually. They are closer to OOP bundling state and behavior together.
I think Erlang did more for practical fp than scala.
My experiences with the encroachment of FP into otherwise imperative or OO languages has mostly been:

1. Write a bunch of functional code.

2. Marvel at how clean and neat things are.

3. Get customer reports of performance problems.

4. Spend a lot of time ripping out FP components and replacing them with imperative code.

What languages? Did you performance profile before to see what was slow?
C++, Python, Kotlin, some others I can't remember anymore.

The most common issue is hidden O(n) situations, which don't show up except in production. So what we end up with is a mix of various paradigms worthy of Dr Frankenstein, and a debugging nightmare.

I've been in engineering long enough to see most trends repeat at least twice (Javascript UIs are ALMOST back to the point of the 90s for example). After awhile you learn to distrust anyone who evangelizses a "new" way that was actually invented in the 60s (and is likely being introduced as a panacea instead of with the original caveats).

Now that I'm a greybeard myself, I finally understand why greybeards were always so grumpy.

> hidden O(n) situations

How are these faster in imperative code?

They are usually not hidden, so they are at least easier to identify.
This is the crux of the matter. Every paradigm is argued about using small program examples, but the real-world issues come as the codebases become large (1M LOC or so). Then the cracks become worriesome.

Imperative non-OO codebases become swamps of maybe (hopefully?) layered code, where each layer contains its own sinkholes and completely bizarre structure (or lack thereof). It's basically a bunch of mini-codebases that meet in an ad-hoc manner, making it very difficult to understand what the hell is going on.

OO codebases encapsulate a LOT better, but working around its straitjacket design becomes harder and harder as you go through rituals like dependency injection, interfaces, adapters, facades, class clusters, and byzantine inheritance hierarchies. Now, rather than a swamp of despair, you have pebbles of functionality. And while these were great when your codebase was 50k LOC, it's now a gravel pile where you can't figure out where anything starts or ends.

FP codebases like to push the details out of the way, making it easier to see at a high level what's going on. But as the codebase grows, you now have multiple iceberg problems, where the devils in the details begin to derail your code in production, and suddenly you find yourself needing to delve down rabbit hole after rabbit hole, chasing issues that weave and bob in utterly chaotic ways due to the lazy evaluation. Reasoning about what it will actually do in what time becomes more and more difficult (putting it in vulgar terms: You're basically exchanging topical complexity for temporal complexity).

TANSTAAFL

Do you pick the lesser of 3 evils, or are there solutions to these cracks?
I haven't worked with a big FP codebase, but I actually thought about this when doing even small toy programs in FP. Sometimes I'm like... do they merge the multiple-sequential-maps that's possibly being done here? Does the code re-use the elements of map that doesn't change? Whereas was it C# or Go, I could just read it.

OTOH isn't that just - you need to know what the function you are calling is doing and how, and read the doc. Whereas in imperative you would implement it yourself, with the pitfalls of that.

If you are coding in Python and you are using a lot of numpy you need to know how to feed data in, how to get data out, etc. so maybe FP programming should treat the STD-lib more like an external library because the building blocks are a bit more high-level?

I've ran into this before, but to be fair I was using Monad Transformers.
That's the common theme even when using clojure too.
(comment deleted)
The big mistake is in believing there is any value in this- or that-oriented programming, or in a this- or that-oriented language.

An engineer uses what is available to produce an economically balanced solution. Done well, that may involve a few OO-ish bits, some functional bits, data-oriented bits, reactive bits, plus hybrid combinations of those and others. The problem dictates the form of the solution.

Every big-enough problem decomposes into a collection of very different subproblems, each of which so decomposes further. The right approach for the top level rarely matches that for most of its parts, nor each for the others, and likewise down the line.

Unless you want to use a different language for each level and subproblem, you will want support for all "orientations" in your language. Thus, a "functional" or "OO" language is an absurdity; likewise, a "functional", "OO", or what-have-you program. They make as much sense as a saw-oriented carpenter's toolbox or table, or a screwdriver-oriented machinist's toolkit or engine.

Programs should be like water, fluidly matching the shape of the problem solved. If that is 20% functional, 10% O-O, and the rest imperative, so be it. Specialization is for insects.

Everything is Turing complete: you can code a solution in any restricted vocabulary. But that means succeeding proves nothing and benefits nobody, but wasted your time and others'.

Use a language that does it all, and use it all. Not necessarily all on every program; but all styles get consideration for each part.

"Subproblem" is likely an exaggeration here, otherwise I vehemently disagree. Take Erlang for example, it's theoretically a general purpose language but highly specialized. Would WhatsApp reach a billion users with 50 engineers without this specialization? I doubt it. It's absolutely fine for things to be heavily specialized, as it is fine for our entire civilization to rely division of labor. As the number of developers increase, so should the number of specializations. While a non-turing-complete language doesn't make much sense, web servers, linear algebra and GUIs for example are sufficiently different so that using the same language is unwise.
There are very successful insects.
No creature is limited to the use of a single programming language, but a lot of programs are. You can learn more than one language and choose one that is appropriate for the task, unlike an insect whose tools are limited so he must have only a few to do everything it needs to. Multi-paradigm is the insectisation here.
From the original post:

> The right approach for the top level rarely matches that for most of its parts, nor each for the others, and likewise down the line. Unless you want to use a different language for each level and subproblem, you will want support for all "orientations" in your language.

It is grossly impractical to use a different language for each fragmentary part of a problem. People do not, as a rule, do that. Trivial exceptions include Python and niche-language programs calling out to C-ABI libraries, programs sending SQL to databases, and web servers sending Javascript to browsers. But those are not about "orientation".

You might use two or more different general-purpose languages for different, independent problems, but that does not contradict my remark about the folly of languages that attempt "purity".

I completely disagree with the kitchen sink language approach. The benefits in many coding paradigms really kick in once they're used 100%.

For example, consider GOTOs. If 99% of your functions don't use any GOTOs but they other 1% does, you can't be sure how the flow of logic lead to any one function. Immutability works in a similar way. I have a far easier time debugging in concurrent code I'm unfamiliar with if it's written in Elixir than if it's written in JS/TS, even if the project in question uses Immutable.js heavily.

> have a far easier time debugging in concurrent code I'm unfamiliar with if it's written in Elixir than if it's written in JS/TS, even if the project in question uses Immutable.js heavily.

How much of that comes from belief rather than reality, and potentially some room to grow on your end? I can debug well written code in any language I jump into, but I've found poorly written and hard to debug morasses can be written in any language.

Poor naming, branch/loop structuring, scoping, data modeling/normalization, module structuring, logging, and dependency selection/integration have all contributed to debugging hassles far more in my production experience than any particular language.

> How much of that comes from a belief rather than reality, and potentially some room to grow on your end?

You know… suggesting that someone that they’re delusional and uninformed isn’t a very charitable way to discuss anything online.

The belief came from personal experience from 2016-2018 while I was much more experienced with JS/TS and relatively new to Elxir. It definitely influenced me to keep exploring and learning things outside the JS ecosystem I’d previously focused on. I’ve grown as a dev since and so has my appreciation of guaranteed immutability.

Also working with other languages, particularly Rust, in the past few years has only further underlined the point. It makes a strict delineation between variables, references and arguments that can be mutated and those that can’t. The language went with 100% guarantees instead of 95% guarantees for a good reason.

> I can debug well written code in any language I jump into

The phrase “well-written” may be doing a lot of lifting here. Different languages and ecosystems make different practices easy and tend to nudge users in different directions. People who don’t like learning often enjoy the idea that any Turing complete language is equivalent from the programmer’s perspective but it’s just not true.

> You know… suggesting that someone that they’re delusional and uninformed isn’t a very charitable way to discuss anything online.

I think there's no reason to exaggerate what I said just because I don't love your pet principle as much as you do. Here's what I explicitly describe as being part of well-written (or poorly written) code, which has very little to do with FP vs OOP vs procedural:

"Poor naming, branch/loop structuring, scoping, data modeling/normalization, module structuring, logging, and dependency selection/integration"

>The phrase “well-written” may be doing a lot of lifting here

I think if you read the entire comment, you'd see it doing a lot less lifting because I explicitly detail what "well-written" is. And I think it is cross-language.

“Everything is Turing complete” isn’t true. You can get quite interesting languages by making sure it isn’t. See strong functional programming for examples.

https://en.m.wikipedia.org/wiki/Total_functional_programming

That you can code certain solutions in a non-Turing-complete language is of identically null benefit.
Aside from being able to prove that the solution halts, you mean? You get a lot of side benefits as well with a halting program, such as provable limits in time and space for the totality of the program instead of just the functional core bits.
Maybe you are unable to prove properties of programs you have coded in Turing-complete languages. Many of us do, though. Routinely.

Quick, does this program terminate:

  int main() {}
? Can't prove it?
That's not a very useful program.
It is the first stage of every useful program.

Add to it, and show that it still terminates, uses finite memory, introduces no data races or undefined behavior, and maybe computes a step toward something useful.

Repeat until done.

I like very much how Wirth puts it, Programs = Algorithms + Data Structures.

Everything else is syntactic sugar.

If that were true, the past 30 years of progress in programming languages would be meaningless. But it is not: a more powerful language enables expressing what a weaker language does not.

A language meant for use by professionals should get powerful features, without regard to any spurious notions of "purity".

Understanding what that equation means in practice is the difference between having a degree and a six month bootcamp as X Developer.
Do you really think you want to go there?
I have a thick skin.
That is not the thickness we are concerned with, here. :)
Can you give any examples of useful non-terminating programs?

It seems to me all useful programs are terminating (or, we truncate some non-terminating procedure by some convergence criteria). The question is only whether we have the logical tools to prove termination at compile time.

Programs running in microcontrollers routinely do not terminate; they run as long as there is power. Even many ordinary programs run until they are killed.

You seem to confuse algorithms, which by definition must terminate, with programs, which do whatever the hell you like (and sometimes things you don't).

Would you mind describing what sort of platform have you experienced such? Was it actually measured that the FP part was responsible for the slow downs?

Let’s be honest, most typical software written by the industry is not the like which has a single hot loop. They do a bunch of different things, usually with small number of entries. I don’t see how FP would be a hindrance to this typical workload.

That's because step 4 is really:

> 4. Spend a lot of time ripping out FP components i'm less familiar with improving the performance of and replacing them with imperative code I have years of experience improving the performance of.

The cause of mist if these kinds of things will be dominated by familiarity for a long time.

> Spend a lot of time ripping out FP components and replacing them with imperative code.

Or use the functional code as a form of tests?

off topic: what is github/readme? medium by github?
The devil as i see is in mixing db transaction with async tasks. Other than that, keep using pure function as much as possible for long run.
How to write a promotional article about functional programming and not talking about Ocaml. Strange.
Learning generalized concepts of FP just make a lot of sense IMO. Knowing that you can map all functors the same way wether it's an Optional, List, Future or Either is a useful tool to have. Using it in a language with syntactic sugar to compose map and flatMap (>>= in Haskell) operations helps to write clean code a lot. For me, solving problems in a functional way is not always as intuitive as the imperative approach but the end result is usually worth it. That said, FP can become a complex mess if your engineers are too dogmatic about it and bringing on new people who aren't as much into FP is almost impossible then in my experience.
IME it is much easier to start with FP and later on introduce OOP rather than the other way around.

And yeah, sometimes stepping down into mutability and such is necessary for performance.

Yes, that is because in FP you limit yourself in what you are allowed to do. You refrain from doing things like mutating state. In OOP you do it all the time and that breaks assumptions about functions. If you then want to do something FP inside that OOP code, you will get impure functions, which do not compose well, because whatever part of the system you interact with from your FP part, you will have lots of side effects being triggered.
(comment deleted)
I’m not sure FP limits you in any way. And I’m not just talking about Turing equivalence.

When you are programming in FP, you are describing the program from another POV - taking the IO monad as an example, it just constructs a “list” of steps that should be taken at a given point vs just listing the steps in the program itself.

In pure FP languages, immutability is enforced due to you describing a given state - it simply doesn’t make sense to change something there.

You are correct in general about FP. I am talking about introducing FP in an existing OOP program though. You might want to use existing parts, but they are usually all impure, with lots of side effects. Any function in which you use those existing parts will be impure as well, unless you resort to things like IO monad. However, that makes it more complicated to implement things. You are building another layer of protection from the existing code and its state mutations everywhere.
I was actually talking about learning, rather than introducing to an existing codebase. But good points anyways.
If Joe Armstrong's Erlang book didn't light the fire, I seriously doubt if it's going to get lit.

Color me skeptical.