https://tutorial.ponylang.io/types/actors.html
And I wonder what makes pony actors different to Go's goroutines?
https://tutorial.ponylang.io/types/actors.html
And I wonder what makes pony actors different to Go's goroutines?
1. Have a typed, nominal interface (called behaviors)
2. Have state
Those are probably the main differences, just with regards to actors.
(What makes Go different from C++ in the 1990s is more community practice around what concurrency patterns are most commonly used, rather than technical differences in the language. Technically Go is just a mutable threaded language; for this purpose details about how the threading is accomplished are not that important.)
In general the big difference with actors is that they are isolated from each other in some manner that means they can't just reach in to some other actor's memory space and manipulate the memory concurrently.
Languages are starting to blur the lines here. Rust's lifetime annotation is not on its own sufficient to call Rust an "actor-based language" but the fact that they also prevent threads/async processes from just reaching in and manipulating other threads/async process's value unexpectedly means you've certainly taken a big step in that direction. And while Go provides no language-level technical support for actors, I still have a lot of things that are de facto actors in Go that uses scoping and export rules to confine the ability of other goroutines to interfere with a particular "actor"'s values, combined with an API that enforces message-based communication even though it works through what look like normal methods. This is not enforced at all technically by the language but it works better with community code than it does in older thread-based languages because the Go community is more recent and you can generally rely on libraries using somewhat more modern concurrency primitives, like providing services based on actors or actor-like objects.
There are some things in programming where if you mostly follow some practice, you get most of the benefit. There are other things in programming where the benefit only kicks in if you do it almost entirely correctly; an example of the latter is Haskell's rigid insistence on functional programming and immutability, I'm not convinced that it's worth doing 90% of that but when you fully commit and build the language around it, there is an interesting spike in capability at the end.
After programming in this space for well over a decade now, I kinda feel like actors are in the former case. I'm not sure you truly need a full language-level dedication to them to get most of their benefits. There is a last level of confidence and surety you can get in a language like Erlang, but I still think in practice that's just a last incremental benefit rather than a sudden burst in utility as you get to 100% purity. I think it's fine to program an actor here and an actor there in other languages and you get the benefits right there on the spot. Even what benefit there is, things like Rust gain in a different way. Still, I've been watching Pony with some interest.
I'm thinking of the early versions of Smalltalk which tilted towards this direction. Why did they abandon 'active objects'? I guess the hardware just wasn't there yet.
By contrast, 90% pure functional programming is a pretty terrible paradigm. You really need to push that number up to get the benefits. As you approach 100% I think the benefit starts to take off. But, speaking very sloppily since "percent pure functional programming" is kind of difficult to precisely define, 90% is like the worst case scenario. 20%, which is to say mixing it in to other paradigms when useful without commitment and without paying much price and just harvesting the low hanging fruit can be very nice, and 99.9% can be very powerful, but 90% is the worst case, where you're paying most of the price but getting few of the benefits of true commitment. This is what I mean by seeing a lot of benefit in going the full way. I don't see this benefit to actors. They work just fine embedded into large programs and deliver the vast bulk of their benefit without having to structure the entire program around them.
Definitely still worth learning; I consider them a core technique in concurrency programming. When you can easily afford the message marshaling and concurrency expenses, they are one of the easiest ways to get concurrency without complexity. And most of the time, you can afford those costs.
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.
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.
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.
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.
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).
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.
> 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.
That‘s just an optimization. You explicitly made the lifecycle requirement part of the semantics.
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.)
> 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.