I've been thinking about this for a couple days, and I think there are a lot of things to like about it — it really keeps the focus on the specific question of whether the value can be sent, and the idea that "this is a value of a non-Sendable
type that can still be sent (because it's known to still be disconnected from any other region)" is really intuitive. The only problem is that I think it might make it really difficult to talk about sendability. That might be a big problem, though.
Yeah, I really worry that it would become difficult to discuss/teach the difference between @Sendable
as a type attribute and sendable
as a parameter modifier when they mean very different things. It's not the case, for instance, that a sendable T
may be passed to a function requiring a U: Sendable
, so it's not the case that sendable T
is Sendable
; huh?
This is why I lean towards sending
—it invokes the idea of sendability while still being nominally distinct. And we already have the -ing
pattern for things that happen 'at the boundary'.
Is there any chance that it’s worth it to rename Sendable
to something like ImplicitlySendable
InherentlySendable
(with Sendable
as a deprecated type alias with a fix-it) while we still can, prior to the great source breakage of Swift 6? Perhaps with all the new work on smoothing over the rough edges of strict concurrency checking, explicitly writing out InherentlySendable
will be uncommon enough that the extra word is not such a burden? It would certainly fix the teachability problem, no? No one would expect a sendable T
to be InherentlySendable
.
No, I don't think there's much of a chance that we're going to rename Sendable
.
One could argue that the current narrative for "talking about sendability" is incomplete, and focusses too much on "making types sendable" anyway. (mainly because it was started before region analysis and improved control around isolation)
In my book, managing the flow of "non-sendable" types through isolation domains is just as important an aspect of "sendability" as actors or Sendable types are.
I understand the worry, it is getting a bit crowded under the @?[Ss]endable umbrella. The question is whether a brand new transferring
keyword - which will need its own explanation - is truly better.
I can understand that line of thinking, but I personally feel that both transferring
and sending
really collide with the ownership keywords. I find consuming transferring
or borrowing sending
tough to read and process - consuming sendable
feels very natural to me.
So one could argue the -ing
keywords (and inout
) are for ownership, but sendable
is not part of the ownership family, so no -ing
.
Could something like ownregion
work? I think adjectival forms (ownregion
, noregion
, disconnected
) make more sense than verbal forms (transferring
, sending
), since it’s more a property of the value than something the function is doing to the value, and if you’re familiar with isolation regions then something being in its own region is fairly intuitive.
Sure, I think you can come up with a consistent explanation either way. My point is less "it would be confusing if we didn't use an -ing
" and more "I think there's an analogy with other -ing
modifiers that would potentially be elucidating for this feature if we deliberately drew the connection". Same thing goes for working send
into the name somehow; I totally agree with your point above:
On this note:
I do wonder how common we'd really expect this to be. Since consuming
is the default I expect consuming transferring
to be fairly rare (since it would only be useful to opt in to explicit copying semantics), and I don't have a great intuition for what borrowing transferring
would be used for. You'd need to maintain the disconnectedness of the parameter, but you can't ever restore that disconnectedness by reassiging (as with inout transferring
)... so all you could really do is call methods with Sendable
local values? Which I guess could still be useful if you have, like, a non-Sendable
type which you want to use to collect some 'breadcrumbs' from a process that takes place across an isolation domain. Perhaps there's a use case for borrowing transferring
that I'm not imagining right now.
Here’s an argument against my suggestion that Sendable
be renamed to InherentlySendable
(even if the source breakage of renaming were not a concern), which I post because I think this line of thinking could possibly stimulate ideas which are relevant to the specific debate at hand:
“The word “Inherently” in InherentlySendable
is redundant. All protocols, by their very nature, imply the prefix “Inherently”. Equatable
should be thought of as InherentlyEquatable
, etc. Thus, the simple fact of the capital “S” in Sendable
is sufficient.”
I was writing a continuation of this type of argumentation as it applies to the meaning of the lowercase “s”, attempting to defend the choice of sendable T
for this parameter modifier, but I failed to convince myself.
Instead, I began to think about sent
as a possibly attractive candidate.
borrowing transferring
as proposed in the current proposal is almost useless —borrowing
only changes parameter ownership, and ownership of the region is still transferred to the callee. As a result, the caller can no longer use the value other than having the responsibility to destroy it. The only reason to have this combination is if you need to adopt transferring
to express a function's region-transfer semantics without breaking binary compatibility.
borrowing disconnected
as I described above, where the borrowing
also changes ownership of the region, is much more generally useful. Consider a function that does a "deep copy" of a non-Sendable
value; such a function should probably express that the return value is disconnected from the argument. func deepCopy(value: T) -> T
doesn't do that; the return value will be in the same region as the argument. Both func deepCopy(value: T) -> disconnected T
and func deepCopy(value: borrowing disconnected T) -> T
express the same thing: there's a default region that all the return values and parameters are in, and the annotation separates a specific return value / parameter (respectively) from that region, and since there are only two return values and parameters in the first place, you end up with the same effective "region ownership signature". But there's a big difference when you have other parameters or when the function is isolated. For example, a fully-general deepCopy
on a graph would need some sort of context that maps old objects to their copies. The return value of that deepCopy
is definitely not disconnected from that context, and I would say that calling the old object borrowing disconnected
is more appropriate.[1]
This is a little bit tricky because the context would need to hold references to the old objects as keys in its dictionary. However, since it doesn't mutate those objects or build new references to them in the new objects, it does preserve the separateness of their region. It would be safe to use
ObjectIdentifier
or a similar trick in the implementation of the context. ↩︎
Just to throw another keyword option into the mix, how about reisolating
? It sticks with the "isolate" base used in SE-0313/0420 and I think adequately describes that the parameter/return value is being moved from one isolation to another.
Nothing interesting! I wanted to demonstrate a successful conversion, but you're right that this isn't a conversion at all.
Yes, that's correct. The compiler will diagnose the point of concurrency where the disconnected region is transferred away (i.e. at the await acceptTransfer(...)
, then note the potential for concurrent access at print(ns2)
. @Michael_Gottesman is also working on diagnostics for region history so the compiler can show a note at the point where ns2
and ns1
are assumed to reference each other at the initialization of ns2
.
I think this is a good question, and it's worth a section in the proposal about how to choose between Sendable
and transferring
in API design. In general, Sendable
is a subset of transferring
because a value with Sendable
type is always safe to transfer. The times when you should prefer Sendable
over transferring
is if you truly need to share the value across multiple isolation domains at once. For example, if you were writing a concurrent map
API, you'd want the closure parameter to be @Sendable
instead of transferring
because you'd want to invoke the closure in multiple child tasks concurrently, which isn't possible with a transferring
closure parameter.
I think in your case, using a transferring
result instead of requiring that the result type be Sendable
will make your API usable with more inputs.
I don't quite understand this question. How exactly are continuations involved here? I know you were comparing your API to the resume(returning:)
API, but I'm not sure what you mean by "locking" your implementation to continuations.
Because a type conforming to Sendable
satisfies the requirements of transferring
, I can't think of a way the API would be less ergonomic by using transferring
instead of Sendable
.
That is a great sentence
I still have doubts, and I'll try to rewrite my initial questions, hoping they are more clear. In the sample codes below, I'll use logger.print
as a placeholder for "stuff that the developer wants to do when debugging/benchmarking/tracing" and ideally would not come in the way.
First, from the point of view of the developer who provides a () -> transferring T
closure. Would the compiler refuse to compile the code below?
makeValue {
let v = NonSendable()
logger.print(v)
return v
}
Same question, for the async variant () async -> transferring T
:
makeValue {
let v = NonSendable()
await logger.print(v)
return v
}
Next, from the point of view of the developer who declares a () -> transferring T
input. Would the compiler refuse to compile the code below?
public func makeValue<T>(
_ value: @escaping @Sendable (Context) -> transferring T
) async -> transferring T
{
let context = await getContext()
let v = value(context)
await logger.print(v)
return v
}
Should we have to try this chance with a separate pitch? Swift 6 seems to be a great point for such renaming after Concurrency incubation period. For me this is similar to the case when AnyObject
was introduced as a better option with deprecation of class
.
I like the idea very much, +1. One more variant - detachedRegion
.
Something with region
word is rather specific and unique to not overlap with domain and current language terms.
also +1
I think sending
makes perfect sense. It’s sending
a value, which means that the value must be sendable. Either because it conforms to Sendable
, or because it’s disconnected.
I personally do not think there is any value in trying to rename Sendable
. The name of Sendable
is fine, and no matter what name it has, there will be a learning curve that folks go through to internalize the term of art and what it means semantically in Swift's concurrency model. Many people have already internalized what a conformance to Sendable
means, and I think changing the name of it now is actively harmful to those who have already put in the effort to internalize Sendable
. The only thing that's changing now is the ability to pass non-Sendable
values over isolation boundaries when there's no potential for concurrent access, and we can bikeshed how to explain the difference between a "sendable value" versus a "sendable type".
I think that the value versus type distinction is one that people have been confused by in the past (e.g. type-level abstraction versus value-level abstraction), and the learning curve may be easier if we use a different term for sendable values whose type does not conform to Sendable
. On the other hand, we use somewhat different terms and very different syntax for global-actor isolation and actor-instance isolation, and people are still confused by the difference between the type-dependence versus value-dependence in actor isolation.
I still think sending
is not the right term because what's needed is a stricter condition than a "sendable value". If this call crosses an isolation boundary, all argument values have to be "sendable", either because they're disconnected or the type is Sendable
. But with transferring
as proposed here, the condition is that the value has to be in a separate region from all other argument values so the callee can merge that value into a region that's different in some way from the other argument values.
That’s a good point. If we want these semantics, I’d say we need something that communicates this properly, i.e. disconnected
.
To me this seems to be the biggest problem with choosing sendable T
. I think the confusion could be attributed to the fact that it looks like the value is of a new type that is related to T, but which has been made sendable, but that’s not quite right, at least according to how types have always worked up until now, because the simple fact of passing the value to another transferring function suddenly “changes its type”, in the sense that the transferability has been removed and now it’s just a normal value of type T
. The sendable
in this case denotes a statically assessed attribute of the value, but rather than the attribute being fixed for the lifetime or the value (like its type is) it can change under a variety of conditions.
Maybe we can change the syntax to somehow reflect this a bit better. At the moment, however, no obviously clearer syntax occurs to me.
I'm not sure I totally grasp the distinction you're drawing here... is it that in the first case ("sendable values") you could hypothetically be passing some non-Sendable
arguments which are all in a region together, which doesn't work with transferring
as-proposed?