The discussion here has been really great, and I think I've changed my mind on some of the syntax in the proposal.
First, I'm in complete agreement with Holly's point about the importance of these two attributes. If we think about this, not from the perspective of where we're coming from, but from the perspective of where the language ought to be, I would summarize it like so:
nonisolated
functions default to running with the same isolation as their caller, and
- there's a specific feature to override that and made them run with no isolation.
@execution(caller)
is a much more marginal attribute, since it merely repeats the default mode of a upcoming feature. It is basically just a transitional attribute that I wouldn't expect to still be appearing in code a few years from now. It should not really be influencing our decision about the best name for @execution(concurrent)
.
Second, I think it is a mistake to use @execution
or @executor
in these names. There are two levels at which to understand how functions are executed in Swift concurrency: the high level of actor isolation and tasks, and the low level of executors and threads. The behavior laid out in this proposal can be completely understood in terms of isolation without talking about executors at all; I explained it in those terms at the top of this post. In fact, trying to understand it in terms of executors can be misleading, both because isolation does not always map naively to executor requests and because executors are used for other things than isolation.
In particular, if you could declare a function as @executor(global)
, it would be very confusing to then have to explain that well actually such a function doesn't always run on the global executor because @executor(global)
is really just a request to be dynamically non-isolated, and a dynamically non-isolated function will run on the current task's task executor instead of the global executor if one has been set.
As a concrete counterproposal, I think we may have been too hasty in dismissing @concurrent
. I understand the objections that concurrency can arise from other things, as well as that, conversely, the execution of a @concurrent
function is not concurrent from the local perspective of the current task. And yet I can't help but think that these objections shouldn't really be fatal. It's true that concurrency can only arise if there are multiple "impetuses" (such as tasks or event sources) in the program that are running with different isolation. But for the most part, we can assume that there are multiple impetuses; and while those impetuses might otherwise share isolation, @concurrent
is the only isolation specification under this proposal that guarantees that they do not and therefore forces concurrency. Indeed, we expect that programmers will be reaching for @concurrent
exactly for that reason: they want the current function to run concurrently with whatever else might happen in the process. So I think @concurrent
really does say something meaningful, and I'm not too worried that programmers will see it and imagine that there can only possibly be concurrency if one of the functions involved is @concurrent
.
As for @execution(caller)
, well, I'm still not completely convinced it's necessary, but if we need it, I think @isolated(caller)
would be a perfectly suitable spelling. It is admittedly a little weird to see @isolated(caller) nonisolated
, if we decide to allow that combination (we don't have to). But it's weird because it's explicitly stating a rule that's weird: a non-isolated function can in fact still be dynamically isolated to something. Unless we deprecate nonisolated
and go in search of other names for the concept — which I agree with Holly would be a mistake — that's just the rule we have, and have always had in the case of non-async
functions. For a transitional attribute that we don't expect to see much in real code in the long run, I don't think that's enough of a problem that we should try to complicate it.