86 karma · joined September 8, 2017
So far as I can determine, this part of the claim is true: it makes no use of locks, not in the runtime and none are surfaced to the Pony programmer. The only synchronization mechanism that Pony makes use of is the MPSC queues that make possible message passing between actors. Each actor has its own queue, and it is only "active" when it has messages on its queue that need to serviced. The work-stealing scheduler will give every actor with messages an opportunity to process at least one message. When it does so, it never blocks and (hopefully) finishes with no requirement to return a result to any other actor (although it may be pass messages to other actors if it wishes).
Now consider Wikipedia's definition of a deadlock: "a deadlock is a state in which each member of a group is waiting for another member, including itself, to take action, such as sending a message or more commonly releasing a lock". Notice that deadlocks can occur not only because of locks (which Pony does not have), but also because of waiting on another actor to send a message. The wiki article lists four conditions for a deadlock, which (it turns out) can also occur in a message-passing architecture.
How would Pony trigger this sort of deadlock? Consider implementing the dining philosophers problem in Pony by having the main actor spawn five fork actors (with on-table state) and then five philosopher actors, telling each the fork actor to their left and the fork actor to their right. The philosopher actors all wake up hungry, so they reach for their left fork first, by sending it an acquire message and then effectively suspend. If the left fork A is in on-table state, it changes its state to indicate it has been acquired by a specific philosopher and sends a message back to the philosopher to say it has been acquired. That reactivates the philospher to now send an acquire message to the right fork B. But maybe by this point in time another philosopher has already acquired that right fork B as its left fork! So it sends a message back to the requesting philosopher to say it is not currently available to be acquired, try again later.
If you walk through this implications of this carefully, you will see that although every philosopher is getting activated by messages regularly, at a certain point in time it is possible that meaningful forward progress can cease for at least one philosopher. At least some philosophers will starve, because they become deadlocked in not being able to get two forks needed to eat, because they are effectively contending over access to shared resources.
The situation is semantically equivalent to when we use locks for forks instead. And if we apply Dijkstra's partial order to how the philosopher actors acquire their forks in an actor-based message passing architecture, the deadlock risk vanishes and all the philosophers have a reasonable chance, eventually, to eat and therefore guarantee eventual forward progress.
Does this explanation of an example satisfy your question?
Notice my description: "Turing proved that no general algorithm can be formulated that can answer this question across all possible programs." The operative part here is "across ALL possible programs". The proof does stop us from knowing that SOME programs will or will not halt. It only stops us from being able to determine this for EVERY possible program. My post explores a very specific subset of programs that we can prove will terminate, then asks what pattern(s) underlie such programs, and then explores the use of such patterns in a variety of interesting problems. It is this last result that most intrigued me, and caused me to write about it.
As for you questions about performance on bigger source files and twiddling optimization options, I too am curious about that. I will likely revisit those questions at some point in the future. It will be easier to do once I have baked these diagnostics into the compiler.
You are correct that Cone supports a static, shared, mutability permission, including on borrowed references into resizable arrays. The short safety answer is array resizing is only possible when you have a unique reference to the array, so you can't run into the trouble you describe. I wrote a post about it.[1]
You left out an important clause I specified in my criteria: "with no chance of exception". Terminating the program in the event of dereferencing a reference does not meet the safety requirements I set for Cone references.
Yes, only borrowed reference have lifetime constraints. I did not mention the allocator-based reference in that quote because of context. Cone does support a distinction between move vs. copy types. Unlike with Rust, the distinction is typically inferred from the definition of the type. Currently, all memory is "pinned", but that may become more flexible in the future.
The safety strategy for Cone involves versatility: giving the programmer a curated collection of permissions and memory allocators, each with distinct advantages and disadvantages. The safety of certain options can be completely determined statically, making them inflexible but fast. Others will use a mix of static and runtime mechanisms, which offer greater flexibility but incur a runtime cost.
That said, I admit I am somewhat uncomfortable temporarily injecting a borrowed reference into a longer-lived container as snippet 4 shows. I feel like any logic able to ensure this is only done safely would be too complicated for my taste, at least for now. I understand how your mechanism would address this scenario, but again that does not ascribe to my more restrictive notion of safety. If the program does it wrong, it crashes.
Thanks for the added insight into your design choices and your kind comments. I will certainly look through your links. [1] http://pling.jondgoodwin.com/post/interior-references-and-sh...
Cone actually does support Rust-like, lifetime-constrained borrowed references which can do exactly that safely. Cone also supports raw pointers (however de-reference safety is the responsibility of the programmer).
I appreciate the chance to learn about your language's unique form of reference type. I am less likely to call them safe than you, no doubt because I use a different criteria for safety. A key requirement I have placed on references (vs. pointers) is that you can always de-reference them and get a valid value with no chance of exception. I don't think your references would comply with this.
A Cone programmer would need to use raw pointers to throw off the shackles of lifetime constraints but, unlike with your references, they could not expect such pointers to turn into nullptr if the object they refer to has been freed. Given the nature of Cone's design, there is no way to accomplish this mechanic with decent performance, especially given that borrowed references and raw pointers are both able to point inside an allocated object.
I do appreciate your bringing it to my attention and wish you all the best with getting others to learn about and adopt your language.
It is fundamentally statically-typed. However, a language that is too rigidly typed can sometimes degrade programmer productivity. Abstractions like variant types and various forms of type polymorphism can improve code flexibility and reuse, which is why I plan for Cone to include these features (the home page has a link to a page that describes this in more detail).
High-level attributes of a language are always hard to get right, because terms mean different things to different people, but several responders do a better job than I did of explaining why concise and readable don't necessarily have to be contradictory goals, at least for me. My primary aim here is that code be maintainable. By concise, I am actually far more focused on how much of a function's logic can fit readably on the editor window without scrolling than I am the size of keywords. Certain common C++/Rust patterns feel unnecessarily verbose to me (e.g., borrowing a reference in Rust), and I am trying to incorporate syntactic patterns from a number of other languages that I feel encode the same intent consistently in a way that a programmer familiar with Cone will be able to process and edit more quickly. Of course, that's always a judgement call...
I do think there are good research opportunities in these ideas, e.g.:
* Type theoretic work on formally proving the soundness of the mechanics (as a follow-on to the proof work done with Pony and Rust-belt).
* Cross-allocator comparative studies of performance and other factors (although many such studies have been conducted in the past, the fact that you can control for the language/runtime and even test hybrid memory management strategies would make a new study a worthy addition to prior work).
* Assessing the impact of borrowed references and lexical memory management on tracing GC's "generational hypothesis"
In your role at Stamford, perhaps you can inspire students to pursue such projects. My focus is devoted to real-world results, but I would be happy to offer support, if that would be of any help.
This is not entirely true. The author of the Gradual Memory Management paper you cite is currently building [1] a systems programming language called Cone [2] that incorporates these mechanisms. Although much work remains to be done, the online reference documentation does go into some detail about the syntax and semantics for references, allocators and permissions.
[1] http://github.com/jondgoodwin/cone [2] http://cone.jondgoodwin.com