I have to disagree pretty strongly about the lack of a "sudden burst in utility" between non-pure actor model and the pure actor model. Erlang's (and Elixir's) VM, the BEAM benefits greatly from knowing, with absolute certainty, that it is impossible to express shared state between two actors.
First off, it's garbage collection system: In Erlang, most actors don't get garbage collected at all until the entire actor is terminated and its memory freed. Long-running processes get handled with a little more grace than that, but it's very performant compared to Java's constant scanning.
Likewise, certain language design decisions allow for optimizing the scheduling system: most obviously, Erlang's complete lack of loops. Want to repeat a computation? Recurse. That choice allows for the scheduler to be far more intelligent, because the new function call provides for a great opportunity for the process's thread to be co-opted.
Another example is Erlang's seamless distribution: since no threads can share memory anyway, there's not a fundamental difference between them running on two different CPU cores or two different machines, save for some added latency.
And, of course, the fault tolerance story. The guaranteed lack of shared state between two actors means that the VM can be certain the blast radius of a failing actor is limited to its context. Wipe its memory, move on. This failure handling model turns handling the error conditions of "invalid input", "missing resources", and "Frank from IT has finally snapped and taken a sledgehammer to one of our servers" into qualitatively the same thing: one or more actors have failed, their supervisor needs to respond appropriately.
All these benefits would be diminished or at least qualitatively different (for the worse) in a language that wanted to walk the middle ground.
Minor nit: In a language like Erlang, function calls (including tail recursion) are instrumented to implement things like preemption, GC, and statistics updates. But loops (backward branches) can just as easily be instrumented, and various imperative language runtimes do so.
Erlang is somewhat unusual among contemporaries in how it multiplexes actors onto system threads. This has historically been quite difficult to get 100% right because even if you manage to make all system calls non-blocking (e.g. TCP socket I/O), "non-blocking" disk I/O can still stall computation for a relative eon. I'm pretty excited about io_uring because it provides a general solution to this problem.
As far as I know the biggest diffeence between Erlang’s GC and other GC algorithms is that the former only has to work on a small local heap.
This also works to a degree in Java with thread local allocation buffers. Objects get allocated on a thread local basis and only move to a shared heap generation when they lived for long enough, or are being shared to another thread.
> As far as I know the biggest diffeence between Erlang’s GC and other GC algorithms is that the former only has to work on a small local heap.
This is also the biggest potential upside of "pluggable", optional GC in a language like Rust. In most programs, the size of a group of references that might form an ownership-relevant cycle is naturally quite small; for the most part memory can be managed with simple RAII, and even refcounting (where multiple "owners" might be extending the lifecycle of a single object) really is quite rare.
Leaving open the possibility of GC allows for expressing more programs, without reducing performance.
> for the most part memory can be managed with simple RAII, and even refcounting (where multiple "owners" might be extending the lifecycle of a single object) really is quite rare
This is absolutely not my experience. I believe there is a bias here between developers using mainly low- or high-level languages, but these nested lifetimes are common in the former because these developers have learned to form such lifetimes, and are biased towards those.
In plenty of areas lifetimes are very very dynamic (any language interpreter/symbolic processing, but even web backends).
Keep in mind that in most GC languages every reference between data objects is inherently managed by GC. That's where much of the overhead comes from, needing to repeatedly trace the full set of program references. Whereas it's absolutely not the case that most references in a real-world program will even be relevant to object lifecycle, let alone be involved in a case where such lifecycle is "dynamically" being extended by multiple objects.
> That's where much of the overhead comes from, needing to repeatedly trace the full set of program references
No modern GC goes over naively the full application, generational GCs basically group together objects and cache their inter-generational references.
Also, I don’t really get what you mean. What reference could be - regardless of language - that is not related to the life cycle of the targeted data? That reference can only be invalid in such case, so, the inverse of the life cycle which is absolutely related. Sure, a pointer can be encoded many smart ways, but in the end they either point to a semantically correct data location or not.
Generational GC's simply have multiple "generations" assigned by age, that get traced at different frequencies - the "younger" generation more frequently than "older" ones. Any caching involved is strictly temporary and does not obviate the need to "go over" the full set at some point. The alternative is a lot of memory overhead.
> What reference could be - regardless of language - that is not related to the life cycle of the targeted data?
If a program reference can be statically proven to never outlive the object it targets, it's per se irrelevant to that object's lifecycle. That's what the borrowck pass in Rust is all about.
Re GC and scheduling: You're not going to like this, but like I said, I worked in this space for many years, and my assessment is that in 2022, these are not relevant. It might be relevant if Erlang was a much faster language, but it's not. It doesn't matter to me that Erlang's GC is hypothetically faster because of its actor model when Go is just straight-up so much faster across the board that it doesn't matter.
Whether a fully compiled language could recover this I don't know, but I doubt there's any room in 2022 to spank a modern GC by more than a few percent as a result of this. By the time you're getting to the scale where this is a problem, everything's a problem anyhow.
Likewise for the "seamless distribution". I don't need it, because the direction everyone has gone is to use other messaging busses like Kafka or the literally dozens of similar products that exist now. I don't need actors to achieve this. The historical accidents of how Erlang achieved this goal are not necessary. This is a classic Erlang mistake, to think that only that one precise space in the design space can achieve these goals, and not noticing just how many other alternatives in this space have straight-up surpassed Erlang in the meantime.
Fault tolerance is another thing that I've been operating on just fine with an 80/20 solution. I actually use my own version of supervisor trees, and while I lack language guarantees of their safety, the truth is they work just fine even so. Language guarantees aren't everything.
It's not that you're wrong in the sense those benefits don't exist, it's just, in 2022, they're not very interesting. It's not 2005 and Erlang is the only practical solution to these problems. It's 2022 and there are an abundance of other solutions, and while they may all have corresponding disadvantages of their own, the probability in 2022 that Erlang is the best choice for any given task really isn't all that good any more. I happily trade away the marginal improvements you mention for 80/20 solutions in Go, while I claim Go's very significantly better performance across the board, for instance. Rust, Node (as much as I otherwise despise it), and many other solutions provide other solutions in the space.
What Erlang gives up to force things into "everything is an actor" aren't things I want to give up anymore. A worthy experiment for the time but I view it in much the same way I view Java's "everything has to be in a class, even pure functions and stuff that ought to be standalone code". Nice try, but there's a reason numerous languages have been introduced since Java/Erlang and don't copy this particular purity. You just don't get the promised win. It's great that we tried it. I wouldn't know we don't get the promised win if nobody had. But now we have, and there isn't some amazing burst of benefit at that last step of purity.
(I keep banging on pure functional programming because it's honestly the only one that comes to mind. In general, you should be suspicious of anyone who claims that particular shape of purity vs. benefits, in practice few things work that way and you should only trust practices that yield marginal benefits as you marginally use more of them, and expect that somewhere before 100% purity the benefits will start toning back down again. Maybe software transactional memory is the other exception that comes to mind; it only works at all with near-total dedication.)
> It doesn't matter to me that Erlang's GC is hypothetically faster because of its actor model when Go is just straight-up so much faster across the board that it doesn't matter.
> Whether a fully compiled language could recover this I don't know, but I doubt there's any room in 2022 to spank a modern GC by more than a few percent as a result of this. By the time you're getting to the scale where this is a problem, everything's a problem anyhow.
It's still relevant if you need consistency. Any GC language eventually has to defragment the heap, and any pervasive-shared-mutability language pretty much has to stop the world to do so. For all that Go is a high throughput language (something that's overstated IME), it's not suitable for realtime, and Erlang is.
First off, it's garbage collection system: In Erlang, most actors don't get garbage collected at all until the entire actor is terminated and its memory freed. Long-running processes get handled with a little more grace than that, but it's very performant compared to Java's constant scanning.
Likewise, certain language design decisions allow for optimizing the scheduling system: most obviously, Erlang's complete lack of loops. Want to repeat a computation? Recurse. That choice allows for the scheduler to be far more intelligent, because the new function call provides for a great opportunity for the process's thread to be co-opted.
Another example is Erlang's seamless distribution: since no threads can share memory anyway, there's not a fundamental difference between them running on two different CPU cores or two different machines, save for some added latency.
And, of course, the fault tolerance story. The guaranteed lack of shared state between two actors means that the VM can be certain the blast radius of a failing actor is limited to its context. Wipe its memory, move on. This failure handling model turns handling the error conditions of "invalid input", "missing resources", and "Frank from IT has finally snapped and taken a sledgehammer to one of our servers" into qualitatively the same thing: one or more actors have failed, their supervisor needs to respond appropriately.
All these benefits would be diminished or at least qualitatively different (for the worse) in a language that wanted to walk the middle ground.