roenxi 12 hours ago

> But again, there is no free lunch: copying data is slow. Very slow. Of course you can optimize data representations, avoid copying binary blobs (they are ref-counted in Erlang) because it would be untenable. But it will still be horribly slow.

You don't have to copy the data; it is immutable. Why copy something that isn't going to change? Are we being charged for empty RAM? Different objects can share the same structure. Famously, this is what Clojure does. Theoretically it can be faster than mutating objects because you only have to write the parts that are changing (which is the same as a mutable object), you lose something to overheads (might be nearly negligible) and have enormous gains in situations where you might need a copy of an object for some reason because that is free; there isn't any reason to actually do a copy unless the data itself is going to be mutated.

  • doctor_phil 10 hours ago

    If you don't copy the data to the other process, then your garbage collection needs to be aware of all processes all the time. I'm not sure if the author would count that as a form of synchronization, but it wouldn't be unreasonable to argue that I think.

    • roenxi 9 hours ago

      I don't think the author took a position on synchronisation at all. He said you have to pick one to abandon of mutability/concurrency/interactivity and that abandoning mutability is horribly slow (because of the need to copy things). And that isn't technically accurate because you can abandon mutability and not copy things. If you have a 1MB int->int associative data structure and need to change one entry, then you can reuse most of the MB (it isn't going to change) and get away with writing a couple of integers.

      And that could be substantially faster than a mutable approach when you need to make a genuine copy of the 1MB structure with a small change and keep both, because you can't structure share and you will need to copy the whole MB. Obviously situation specific, and I'm sure there are edge cases where immutable lookup tables fall apart. I haven't met one myself.

      I don't disagree with the article exactly, I do think abandoning mutability is necessary. I just don't think his argument that it needs to be slow is good. If you abandon mutability, "copying" data logically is extremely fast because you can skip almost all the IO for large objects. And copying a large object with a small change should be much faster in an immutable language because of structure sharing. Depending on the workload, that might be faster.

  • vmg12 9 hours ago

    Elixir / Beam take this approach with byte strings. Once they exceed 64 bytes they are allocated to shared reference counted memory.

    • gchamonlive 9 hours ago

      I'm more and more convinced that for these types of problems most popular languages are just reimplementing or offloading to services like Kafka or Celery functions that are first class primitives in the BeamVM/Elixir

socketcluster 12 hours ago

You can substitute the word 'interactivity' with 'statefulness' and it makes the statement more intuitive IMO.

Concurrency + statefulness means you can't have mutability because then you would run the risk of a concurrent update overwriting another. But you can have concurrent read-only state, so long as concurrent writing is not permitted. This is the 'single writer principle' which is a variant of the 'single source of truth principle'. You can't have concurrent writing in this case.

If you have statefulness + mutability then you must do away with concurrency; a different way to avoid the same concurrent overwriting problem mentioned above.

If you have concurrency + mutability, then that's possible if you don't have state... You could have multiple independent, divergent copies of the state but not a single consistent state.

  • PunchyHamster 11 hours ago

    > If you have statefulness + mutability then you must do away with concurrency; a different way to avoid the same concurrent overwriting problem mentioned above.

    Last time I checked we had plenty of concurrency primitives allowing for that; you might need to wait few tens or hundreds of nanoseconds for a lock (or few orders more over network), but it works just fine

    • naasking 9 hours ago

      All of them depend on temporarily suspending concurrency in order to synchronize. Even atomic exchange operations are like this at the hardware level.

      • gpderetta 9 hours ago

        Are you implying that a single CPU system can't be concurrent?

        • naasking 9 hours ago

          What sentence from my post implies that?

  • mrkeen 10 hours ago

    'Statefulness' doesn't make anything clearer.

    What exactly are you mutating?

    • dwattttt 8 hours ago

      I'll take a punt; based on the last pairing, where "statefulness" is abandoned

      > You could have multiple independent, divergent copies of the state but not a single consistent state.

      I'd suggest it's describing "consistency".

mrkeen 12 hours ago

This doesn't follow at all!

  Dropping interactivity [keep Concurrency, mutability]:

  The most popular choice is to drop interactivity: since there is no one to randomly access runtime data, there are no concurrency issues. C, Rust, Go, the list of languages that go this way is long.

No-one to randomly access runtime data? Do "other threads" not count? There are no concurrency issues in C?

  Dropping mutability

  What if you could not really change data? Instead of reading the value of a variable, you could have the runtime systematically (and safely) copy the value and return it to you.

Why would you copy anything? Copying is defense against mutation. It's what you do for safety when you're working in a language with pervasive mutation, and it's opt-in and manual, meaning it works about as well as other opt-in and manual operations, like malloc and free.

Peeking over the fence at an immutable language and thinking "that runtime does too much copying, which is bad!" is like peeking over the fence at a GC language and thinking "that runtime does too many mallocs and frees, which is bad!"

  • phil-martin 12 hours ago

    I believe the article is written from a lisp point of view where it interactivity is inspecting and modifying the application itself while it is running. It’s assuming that the software has already been written with no existing concurrency issues, but when a person comes along and does something unexpected and rewrites pose of the application or in the example they gave, modifying a hash map, things break.

    They used C as an example because under normal circumstances you compile a binary and don’t modify it at runtime.

    I’m not doing to defend it to strongly though, I _think_ that is what they were getting at, but to be honest I found much of it confusing.

    • Certhas 12 hours ago

      I think the example makes it pretty clear that this is exactly what is meant. I was thrown by the term interactivity at first as well. But I think the trichonomy is sound, and the Erlang Vs Python Vs Rust buckets are quite meaningful, though I would argue it's rare for people to "interact" with running Python programmes in this way.

    • quadhome 10 hours ago

      It's especially weird since the author is talking about inspecting and modifying data at runtime; gdb happily attaches to a process running a program written in C. However, if he were talking about inspecting and modifying the program itself, then we'd be cooking.

    • mrkeen 10 hours ago

      If the author wants to take "a C program with concurrency and mutability is fine" (correctness being left to the reader), then he might as well just put a big global lock around the REPL and declare interactivity fixed too!

my-next-account 11 hours ago

You can have it all in the vast majority of cases. It just requires a lot of work. See: The Hotspot JVM.

  • officialchicken 10 hours ago

    Totally agree; we're approaching a time where a single processor has hundreds of threads - it's a conceptual limitation of the author's preferred language that was never considered due to it's ancient 75-ish year old design. "Use the right tool for the job" has never been more important, versus "I have a hammer, everything is a nail"

    • zrm 9 hours ago

      > we're approaching a time where a single processor has hundreds of threads

      Approaching? EPYC 9996 has 512 threads. EPYC 9754 had 256 threads three years ago.

strawhatdev 9 hours ago

I really like pony's approach to this tradeoff. Reference capabilities provide such a nice semantic lever for thinking this stuff through.

Wish the language was more popular!

eqvinox 11 hours ago

RCU needs discussing in an article like this. It arguably fudges the definitions of all 3 things a little, but you do get them all.

okkdev 11 hours ago

That linked benchmark feels weird. The discrepancy is way too big. I'll need to take a look into the slower contestants.

chrisjj 3 hours ago

> Unfortunately another thread happens to read the hash table while the form is being evaluated.

Article omits to say what the form is.

ordu 9 hours ago

I believe it is a Lisp related story, not some general truth. I mean, if I had a hashtable that needs to be mutated asynchronously, I'd probably create a single writer to it. Some entity that takes ownership of a hashtable and you can talk with hashtable only through it.

But if you are writing Lisp, and that hashtable is a global, then good luck ensuring that no one except the designated entity talks with hashtable directly. In particular, you can't force the user at REPL prompt to not do it.

The mutability lament in the same ballpark. Dynamic nature of LISP makes it hard to ensure that some code paths can't change values they deal with. Statically typed languages do it all the time: just pass a const reference into a function and you can be sure that it wouldn't change the value, or anything referenced from it. In LISP the easiest way is to make a deep copy of a value. Other ways would probably need some changes introduced into lisp-machine itself. So you are choosing between two alternatives: either copy everything, or to believe other code to behave like values are immutable. The former is slow, the latter just doesn't work generally.

Take Rust for example: if I have a mutable HashMap that needs a concurrent access, I'd hide it. It would be accessible directly in one module only, and the module would export safe API that you can't use to achieve data race. Technically I can make this HashMap to be global, but then Rust would rebels and force me to wrap my HashTable into a Mutex. If I added REPL to the mix, then REPL would need to lock Mutex as any other thread.

Lisp just an ancient language that doesn't have these guardrails, so you need to think things through and ofc you will be overwhelmed in any real case by all these things, and then you write that you can't have concurrency, interactivity and mutability at the same time. To have them, you have to limit yourself in how you write your program, and while you can use LISP to limit yourself, you still need to do the decision and invest some work into it. It doesn't happen magically all by itself. For magic you'd better try rust, though its "magic" requires months of fighting with borrow checker until it succeeds at conditioning you into right practices.

gpderetta 9 hours ago

A reminder that databases (as an example) provide all three without issues (at the cost of complexity in the implementation of course).