Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

If your concurrency model only allows bounded nondeterminism, you can't express an algorithm that assumes a concurrency model with unbounded nondeterminism in it. Furthermore, the choices concurrency models make hugely affect performance, so it doesn't even matter much if models were equivalent. So, no, algorithms do have to be explicit about concurrency models, whether you like it or not.


That some piece of code is explicit about what it does and that the capability to do so must be declared ahead of time in the type system are two different things. For example, every subroutine must be explicit about what it does and what, if anything, it returns, yet languages differ on how much of that must be declared in the subroutine's type signature -- Haskell requires being explicit in the type signature about return types as well as effects, Rust is explicit about the return type and some kinds of effects, Java is explicit about the return types and a smaller subset of effects, and JavaScript is explicit about neither. A language can provide the same suspension (or "await") behavior as Rust's async/await without requiring subroutines that use that mechanism to declare that they do so in the type system. Scheme does precisely that (with shift/reset).

In fact, Rust already gives you two ways to suspend execution -- one requires a declaration, and the other does not, even though the two differ only in the choice of implementation. That you wish to use the language's suspension mechanism rather than the OS's is not a different "model." It's the same model with different implementations.

Whether you need to declare that a subroutine blocks or not has no bearing on the fairness of scheduling.


Type declaration is supposed to signify the choice of concurrency model. Either way algorithmically the choice has to be explicit, even it it's not a type declaration, it still has to be an explicit declaration somewhere or there is no choice and algorithms have to be expressed in terms of a single concurrency model.


> Type declaration is supposed to signify the choice of concurrency model.

That "supposed to" is an aesthetic/ideological/pragmatic/whatever preference, and one with significant tradeoffs. Again, Scheme's shift/reset gives you the same control over scheduling as Rust's async/await, but you don't need to declare that in the type signature. Rust puts it in the type signature because in the domains Rust targets it is important that you know precisely what kind of code the compiler generates.

> Either way algorithmically the choice has to be explicit, even it it's not a type declaration, it still has to be an explicit declaration somewhere or there is no choice and algorithms have to be expressed in terms of a single concurrency model.

First, we're not talking about two models, but one model with two implementations (imagine that the OS could give you control over scheduling, like here [1]; you'll have the exact same control over the "concurrency model" but without the type declaration -- even in Rust -- although you'll have less precise control over memory footprint). Second, that the language's design dictates that you must tell the compiler in the type signature what it is that your computation does is precisely what creates accidental complexity, as it impacts your code's consumers as well.

[1]: https://youtu.be/KXuZi9aeGTw


With "async/await" you see exact places where you give up control, it's part of the model, without it you don't and it's a different model. First one lets you program as if you can always access shared memory like you are the only one doing it, since you always know exactly at which point this assumption no longer holds, second one requires you to be more careful about any function call, basically limiting you to the same assumption but only for code in between function calls, reducing power to abstract things with functions. Things get even worse with different executors, that give you completely different concurrency models and break all the assumptions of a simple event loop executor.


You're not talking about different algorithmic models, just about expressing constraints (e.g. you could follow the same algorithm, with the same constraints even without enforcement by the type checker).

In any event, the reason Rust does it is not because of the aesthetic preference you express (that would be true for, say, Haskell) but because the domains it targets require very fine-grained control over resource utilization, which, in turs, require very careful guarantees about the exact code the compiler emits. It's a feature of Rust following C++'s "zero-cost abstractions" philosophy, which is suitable for the domains Rust and C++ target -- not something essential to concurrency. Again, Scheme gives you the same model, and the same control over concurrency, without the type signatures, at the cost of less control over memory footprint, allocation and deallocation.

I think you might be saying that you happen to personally prefer the choices Rust makes (which it makes because of its unique requirements), and that's a perfectly valid preference but it's not universal, let alone essential.


These are not aesthetic preferences, these are essential parts of concurrency models. And they cannot be decoupled from algorithms.


Scheme has shift/reset which is the same as async/await (in fact, it's strictly more powerful) without requiring any declarations in the signature. Again, the choice of algorithm is one thing, but what adds to the accidental complexity is the requirement that that choice be reflected in the type signature, and so it affects the consumer of the code as well. Reflecting the algorithm in the signature is an aesthetic choice in general, and a forced choice in Rust given the domains it targets.


> Again, the choice of algorithm is one thing, but what adds to the accidental complexity is the requirement that that choice be reflected in the type signature, and so it affects the consumer of the code as well.

As long as you have shared memory adding type signatures to avoid thinking about and handling concurrent memory access only reduces accidental complexity and limits possibilities to make mistakes.


> As long as you have shared memory adding type signatures to avoid thinking about and handling concurrent memory access only reduces accidental complexity and limits possibilities to make mistakes.

That is certainly one way to reduce algorithmic mistakes, but it doesn't reduce accidental complexity, and it's not why Rust requires async in the signature. Rust needs to prevent data races even in non-async subroutines that run concurrently. Rust requires async because it implements that feature by compiling an async subroutine rather differently from a non-async one, and the commitment to so-called "zero-cost abstractions" requires the consumers to know how the subroutine is compiled.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: