SE-0430 (second review): `sendable` parameter and result values

I think with adopting it's clear that we'd need a second keyword for the return position, eg.

func f(x: adopting T) -> orphaning T { ... }

Which, I mean, I think that improves clarity. I get the argument about "use the same word for the same concept", but I'm unconvinced that I'll be thinking about this as "the same" as Sendable in practice β€” more likely I'll be thinking about it in terms of "avoiding Sendable".

IDK, I think I've arrived right back at transferring being the right spelling :sweat_smile:

I'm also unconvinced that the keyword should go on the type in argument position, it seems more like it should go on the argument (there is some precedent for this in result builders):

func f(sendable x: T) -> sendable T { ... }

(which is even better if you choose other keywords too)

1 Like

Can we at least make Continuation.yield(with:) consuming by keeping the old ABI name around with the old implementation and introduce a new method with the same API name but a different ABI name though @_silgen_name or similar?

isn’t it already allowed to overload on consuming?

Yes, but it will become ambiguous at the call side.

1 Like

Am I correct that this proposal would allow both:

func foo(_: @Sendable () -> Void)
func foo(_: sendable () -> Void)

And they would mean different things?

If so, I worry that it may be quite a fine line. It might be difficult to teach.

9 Likes

I am not sure how I feel about the proposed sendable spelling. Taking the following extreme example:

func foo<A: Sendable, B>(_ operation: @Sendable (A) -> sendable B) 

I know this isn't what one would write regularly but just trying to explain what this method does and what each different Sendable/@Sendable/sendable annotation seems hard.

I am not saying transferring or any other suggestion here is better but I just wanted to spell out an extreme example that we have to be able to teach people.

6 Likes

[Review manager hat off for this post.]

Yeah, I’m worried about this too. I think we might want to step back from using precisely sendable and instead use some other variant of β€œsend”. I’m not super-fussed about exactly which of those words we use, but sent might read a little better on e.g. property types (which might allow this keyword in the future) than many of the alternatives.

4 Likes

Just to add to what you said here Becca, this is what the various options look like graphically on properties (just for the purpose of helping to visualize during the review, I am not taking a specific perspective):

struct S {
  var x1: transferring MyKlass { ... }
  var x2: sendable MyKlass { ... }
  var x3: sending MyKlass { ... }
  var x4: send MyKlass { ... }
  var x5: sent MyKlass { ... }
}
3 Likes

I believe in the prior review the possibility was raised of giving borrowing senable a different meaning than this; namely, a borrowing sendable value would also 'borrow' the isolation region and return it to the caller undisturbed (rather than merging the region into the callee). Is the intent to rule that out as a future direction? If not, could we provide the 'preserve ABI but still merge the region' functionality via a private underscored modifier/attribute and give borrowing sendable the alternative meaning? Or, if the intent is to affirmatively rule out that direction, why was that decided?

1 Like

No, this is something that has not been ruled out. We were thinking of perhaps calling it 'preserved' (don't take the word used seriously) instead of 'borrowing'. Borrowing is an ownership concept so we do not want to overload it with meaning based off of context here.

3 Likes

They're two different points on the same spectrum, yes. A @Sendable function has a sendable type, which means it must be innately thread-safe: it can be sent around and used concurrently (i.e. called from multiple threads at once). A sendable function is a sendable value, which means it does not have to be innately thread-safe, just separable from the current context: it can sent around to new concurrent contexts but can only be used by one context at a time.[1]

As we've gotten more experience with the impact of region-based isolation, we've come to realize that it's a much more fundamental part of the concurrency design toolbox than we initially understood. For better or worse, the sendable naming and this concept of sendable values vs. sendable types is an attempt to come to terms with that.

As a practical example of this, in Swift 5.10, the Task initializer takes a @Sendable function. That seems to make sense because calling the function on a new task that will run concurrently with the current context is a send, right? But it's actually overly-strict: the task function is not called concurrently (in fact, it's only called once). So the right model is that it's a sendable function, and it's okay for it to capture non-Sendable state as long as that state is only used from the new task.


  1. For function values specifically, sendability is mostly a restriction on the captures. A @Sendable function must either not capture anything non-Sendable or be isolated to an actor (in which case such values are merged into the actor's region). A sendable function can capture non-Sendable values, but they must be disconnected from everything else. There are some future directions here that could allow those values to then be transferred again when the closure is called. β†©οΈŽ

14 Likes

Just to pull on this thread a little bit, since this proposal as written would rule out the borrowing sendable naming, at the very least. In the previous review borrowing sendable was called "almost useless" as-proposed. If we were to use the 'preserved' terminology, is consuming preserved something that would make sense, or would that also be almost useless? While sendable and preserved aren't strictly about ownership, they are also not entirely unrelated: there is a reason that sendable also implies consuming.

ETA: I don't think this has to be resolved or justified as part of SE-0430, necessarily. But I'm wary of 'burning' the borrowing sendable syntax solely in the name of an ABI compatibility kludge that could also be accomplished with an underscored modifier. Unless there's a high degree of confidence that borrowing sendable is really, truly the wrong way to spell this, I'd rather make it an error for now and leave it open to future design work.

2 Likes

Just to emphasise this. I have recently spent some time looking over all of our Sendable and @Sendable annotations in NIO and came to the realisation that a lot all of them can be replaced with sendable/transferring instead. We often don't have to concurrently access the value but we just need to make sure it is transferred from the callers isolation domain.

6 Likes

I also recall there being some debate as to whether swift-system's FileDescriptor should be Sendable. There are compelling arguments on either side.

The ability to transfer values across regions (and values of non-sendable types became more usable as a result) could mean it is better to keep non-Sendable.

3 Likes

Yeah, I don't actually begrudge the discussion of sendable values vs. sendable types: I think that's a really great way to think, talk, and teach about the concurrency model. But I don't think that sendable as a modifier attached to a parameter type, does basically anything to evoke that distinction. Explaining that a type written as sendable T doesn't actually describe a type T that is 'sendable' feels quite silly to me. IOW, to the extent we're trying to illustrate this spectrum:

I don't think a single capital/lowercase letter of difference does us any good. OTOH if we keep Sendable as a concept which exclusively applies at the type level, while still talking about 'sending' more broadly, I think it more naturally raises the right questions: e.g., "This parameter type is marked sending/sent/send, but the type isn't Sendableβ€”so how can a non-Sendable type be sent?"; "Well, the compiler needs to be able to prove something about the specific argument values which ends up being passed here at the call sites in order to be certain that each usage won't introduce data races."

Perhaps I'm over-narrativizing, but the more I think about it the more I prefer the story I can tell if we don't reuse sendable for this purpose.

3 Likes

It isn't just an ABI compatibility kludge. That is just a nice side-effect of the actual ownership semantics. More importantly it provides no implicit copy semantics like consuming does. I view it as an orthogonal semantic to region based isolation.

That being said, I would argue that it would be actively harmful to make borrowing imply anything about the region something belongs to. It is mixing the ownership layers and the region layers of the language, effectively adding a special case to borrowing. In my opinion, we should avoid more special cases unless it is really necessary and this misses the mark for me. Someone should be able to work with ownership outside of concurrency and with ownership in concurrency and know semantically that borrowing has the same effect.

Just to add to this, would borrowing without transferring when one crosses an isolation boundary mean preserving as well? If we were to use borrowing this way, we would effectively burn that part of the API design space. Example:

actor MyActor {
  var myData: MyNonSendableData
  var otherDataFromNonSendableData: OtherData

  // I am doing something performant here and I want to ensure that the copies introduces
  // are known and explicit.
  func doSomethingPerformant(_ x: borrowing MyNonSendableData) async {
    // ... do stuff ensuring that no copy is performed ...

    // If borrowing means preserving, I can't do this.
    myData = copy x // One copy.

    // Or this:
    otherDataFromNonSendableData = x
  }
}

func f() async {
  let a = MyActor()
  let x = MyNonSendableData()
  a.doSomethingPerformant(x)
}
2 Likes

Sure, but that would already be available as consuming sendable no?

It seems to me like the design in this proposal is what makes borrowing sendable 'different'. Normally, borrowing values are returned back to the callee for continued use after a call completes. But a borrowing sendable value is merged into the isolation region of the callee, preventing further use. IMO borrowing sendable which allows use after the call is more expected.

1 Like

No because consuming implies a +1 parameter. I realize that you posted before my edit above where I actively use the +0-ness (my apologies).

To me that isn't what borrowing means. Borrowing to me means that from an ownership perspective, the value will not be destroyed by the callee. It doesn't state anything about how you can use it in the caller afterwards. The fact that you can do it normally is not a result of borrowing itself. The transferring rule here to me is overlaid on top of that borrowing rule.

My gut instinct is that sendable will be the most common, by a significant-enough margin to kind of become the default. I suspect that a genuine desire for @Sendable in this position will be rare; it might be used more often because the compiler [seemingly] demands it.

Which leads to the question of cargo-culting susceptibility - if people habitually write sendable and the compiler says a specific use isn't sendable, are they going to understand that probably they should reconsider how the value is used after the call (i.e. try not to use it after the call, or anything it's connected to, so that it can just be implicitly sendable) or are they going to dogmatically try to slap @Sendable / Sendable (the protocol) on things until the compiler stops complaining?

Beyond that, I think that their relationship is conceptually pretty simple and clean - "sendable" is essentially a superset of Sendable / @Sendable types. Logic 101; all Sendables are sendable but only some sendables are Sendable. This is already a very common bit of logic in Swift (and most popular languages), e.g. type inheritance, Any vs AnyObject, etc.

1 Like

Oh, perhaps we are talking past each other? I am not suggesting that borrowing should imply preserving/sendable, just that borrowing sendable be given the meaning of preserving (or borrowing preserving).