SE-0302 (second review): Sendable and sendable closures

I completely agree.

The other big problem with calling this "concurrent" is that this isn't what the attribute or protocol do or enable. The types themselves aren't "concurrent", this feature allows a (function or nominal) type to be "passed across concurrency domains" by copy.

As others have pointed out, the name "Sendable" is quite generic and doesn't convey much about the use case of concurrency, but this will occur in many places in the code and will become a term of art quickly. I'm not worried about it being confusing or ambiguous, but I do think it is important to align the terms.

-Chris

5 Likes

Not only that, but the following is, well, chef's kiss:

This is exactly the perfect, desired response! A user encounters for the first time a protocol that will be commonly used over and over again. They immediately:

  1. grasp the explicitly communicated part about the semantics of the protocol (that it's about a type that can be sent somewhere)
  2. then ask exactly the right question rather than assuming an incorrect answer (sent where? how?)
  3. which prompts them to look up the documentation to get all the details about the semantics.

Consider the possible alternatives:

  • A user reads ValueSemantics and confidently assumes a certain definition of value semantics that differs from the documented semantics
  • A user reads ConcurrentValue, learning that concurrency means parallelism
  • A user reads ActorSendable, has to look up the documentation the first time anyway (it is, after all, a special marker protocol with no required members that could even hint at how it's used, so how does one "send" a sendable value?), and gets an unnecessary clarification that sendability has to do with actors for all (n - 1) times after that when they encounter the protocol.
7 Likes

I'm not sure where the idea came from that these restrictions are only interesting under parallel execution. Essentially the exact same memory-safety issues come up with preemptive scheduling on a single processor. Even under cooperative scheduling, with interleaving only at suspension points, whether memory is isolated or not is extremely semantically important.

The feature talks about concurrency because the issues arise from any form of concurrency.

8 Likes

I have to chuckle at this optimistic view of developer behavior!

I do tend to agree with Xiaodi that Sendable prompts good questions. I still feel some pull toward more specific names that make it clear that it’s concurrency and/or parallelism (hi Joe) that’s in question: AsyncSendable (still like it: you can send it to something that’s async!), or ConcurrencySafe
something along those lines.

I take it I’m the only fan of those sorts of names, however. If we’re sticking to the more concise names, I do tend more toward Xiaodi’s general philosophy that it’s better to imply too little and prompt good questions than to imply too much and prompt misuse. And concur with Chris about this:

There is Dispatchable if we are willing to reuse this word given GCD. I think Sendable is fine and aligned with Send in rust. I think the use of ‘unchecked’ as a term is more problematic for me.

Overall, I am +1 on this proposal.

One thing I did notice reading the proposal is that the words 'transfer' and 'transferring' are used 24 times (not including the table of contents) including ten uses of variants of the phrases ‘transfer across concurrent boundaries’ or ‘transfer across concurrency domains’.

That phrase is cited as part of the key question in the Motivation section and the summary of the purpose of the proposal in the Conclusion.

Outside of mentions of Sendable and @sendable, the word ‘send’ or its derivatives only appear five times in the proposal.

  • Twice ‘send’ is used as a noun: ‘actor message send’
  • Twice it is used paired with ‘receive’ (send/receive, sender/receiver)
  • Only once is it used to describe wanting to ‘send bits of computation between concurrency domains’

The proposal text itself favors the term ‘transfer’ to ‘send’ as the best way to describe passing values between concurrency domains.

To me, that indicates that Transferable and @transferable may be better names for the protocol and attribute.

Two comparative drawbacks I can think of to this name are that it is three characters longer and some people will probably want to spell it with two ‘r’s.

A few other points from reading the proposal:

I also noticed that of five mentions of the word ‘send/sender’ in the proposal text, two of them are paired with ‘receive/receiver’. In this proposal, a value that is ‘sendable’ is also implied to be ‘receivable’. A value that is ‘transferrable’ explicitly acknowledges both sides of the transfer.

The examples I’ve found of types that mention one side of an operation tend to be paired with the other side, such as Encodable/Decodable and Foundation’s InputStream/OutputStream. It does seem a little odd that many things will be Sendable but nothing is Receivable.

For me Transferable better describes the entirety of the operation. It marks something I can transfer from one context to another.

And although I realize proposal review should not focus strongly on the follow-on work mentioned in the proposal, I think it’s important to note that the naming decided on in this proposal will set precedent for the naming of future related types.

For instance, I would say that the naming of the UnsafeTransfer property wrapper described in the Future Work / Follow-on Projects section is an excellent, descriptive name.

With the protocol name Transferable, it follows that something transferable is part of a transfer. Something that is not marked as transferable would need to be part of an unsafe transfer. Types and APIs can refer to transfers and transferable values and they fit in the same family of names for related functionality.

The analogous type, I suppose, would be an UnsafeSend where send is used as a noun and not a verb. I’ve typically not heard ‘send’ used as a noun on its own, but in a phrase like ‘message send’. Of course, this property wrapper would not be an UnsafeMessageSend because the message isn’t unsafe, it is the value. So it could be an UnsafeValueSend.

I think most would agree that UnsafeTransfer would be a better name than UnsafeValueSend, but using that name the developer now needs to associate ‘sendable’ with ‘transfer’.

Conclusion

Based on simply the names themselves, I find Transferable to be more descriptive and preferable to Sendable.

For me an even stronger argument for Transferable over Sendable is that it opens things up for much clearer naming of follow-on types and API, with the clear association of a ‘transfer’ with ‘transferable’.

9 Likes

Without arguing against your point, I'll explain why: a much earlier draft of this had proposed the named ActorTransferrable. I realigned much of the writing around "transfer" because of that. We subsequently renamed the thing (a couple times) and didn't update the verbs.

-Chris

3 Likes

Maybe call it Passable then? :wink: But I think Concurrent was a better word.

I don't like Sendable very much because the word "send" is about too many things (send data on the network, send an email, send a signal) and now we'd make "send to another concurrency domain" the privileged meaning, despite most people now knowing what a concurrency domain is.

The thing is, we're not really "sending" the value anywhere. It's a metaphor to say a shallow copy can be used concurrently from another domain. Sometime you have to capture it in a closure or pass it as a function argument to make it available to another domain (hence the "send" meaning), some other time it'll be a global constant, or an actor's "nonisolated" constant, that can be used from any domain without anyone "sending" it anywhere.

(If we wanted a clever word, we could say the value is Transcendent across all the domains.)

1 Like

Thank you for that clarification @Chris_Lattner3. I had not seen the earlier revision.

It's an amusing progression that an earlier proposed name led to the heavy use of the verb in the proposal text which led to the suggestion of a variation of the original name.

But I am glad the proposal text led me to think about Transferable as a name.

I realize the 'actor' part of ActorTransferable doesn't take into account that not all concurrency domains are actors.

But, for the reasons stated in my earlier post, I do think that Transferable works better for the name. Maybe it will make a comeback.

Bikeshedding... (I think the proposal is very exciting and super promising, but I'm not fond of the naming.)

IMHO it is a very valuable trait of the "concurrent" naming that the reader immediately knows what general area this belongs to. It may not be strictly correct, like a ResultBuilder isn't just for building results, but it is clear and not easy to confuse with unrelated functionality.

I specifically do not think this is about moving stuff around, like "sending" or "transferring". It's about being "safe" or "pure" so you can safely use it with Swift Concurrency: with async/await, with Tasks, and with Actors.

I vote for bringing back the "concurrent" naming. I would like something like "Concurrentable", but as that doesn't exist, I vote for:

  • @sendable -> @concurrent
  • Sendable -> Concurrent
  • @unchecked Sendable -> @unchecked Concurrent

If "concurrent" is ruled out for some reason, my vote would be for "pure":

  • @sendable -> @pure
  • Sendable -> Pure
  • @unchecked Sendable -> @unchecked Pure

class MyThreadSafeThing: @unchecked Pure { ... }

  • What is your evaluation of the proposal?
    +1
  • Is the problem being addressed significant enough to warrant a change to Swift?
    yes
  • Does this proposal fit well with the feel and direction of Swift?
    yes
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
    I think the simplicity here is fantastic.
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
    Unfortunately catching up late due to busyness. Though read this current proposal thread and the proposal in full.

My 1 and only question mark revolves around keypath literals.

However, to ensure that it is safe to share key paths, key path literals can only capture values of types that conform to the Sendable protocol. This affects uses of subscripts in key paths

I'm wondering why this kind of breaking change could not be avoided? I'd assume that only where keypaths are used within some concurrent context that captured variables would require Sendable. Possibly this was covered in a previous review, and if so, my apologies.

Thanks, and truly awesome proposal overall.

Or perhaps:

  • @sendable -> @concurrentSafe
  • Sendable -> ConcurrentSafe
  • @unchecked Sendable -> @unchecked ConcurrentSafe

+1, I'm very happy with how this proposal is coming together overall :slight_smile:

The name

It's fine :wink: :+1:

We have had numerous names in something I worked for this concept. Including the quite "pure" from a model perspective ActorMessage but here we're talking about not only specifically actors, so something around "sending message" sounds fine, and as such Sendable sounds good to me :+1: It definitely isn't "pure" or value type as was discussed a few times already, there are various ways to ensure a value is safe to be sent around between actors, so no name that describes the specific implementation technique will be a good fit here.

typealias DistributedSendable would be for example Codable & Sendable which I also quite like.

Future: How do we express "not-Sendable"

In presence of automatic Sendable derivation (which I think is the right thing to do, thanks so much!), there may be very rare occasions where one would want to prevent a type getting automatically Sendable conformed.

In rust this is done by

#![feature(negative_impls)]

struct Thingy(...); // has some magic meaning and should not be shared/sent

impl !Send for Thingy {} // Thingy is NOT Send

I guess the existence of "modifiers for conformances" opens up the door for this... The same way we can conform to actor X: isolated Protocol we could invent some spelling for this "opt out" conformance; Spitballing here... struct DontSendMe: not Sendable :wink:

Are we considering such thing as well, or rather leaving it out until some later point? I guess it's a rare situation, but it can happen so some way to spell it would be nice to have the model be complete.

1 Like

How about struct DontSendMe: !Sendable
is it possible?

bang in Swift while means negation in some places — I’d argue that most of the time when I see bang in Swift it is about forcing something like try! Or force unwrapping.

class ImAmSafe: Sendable! {...} // instead of unchecked Sendable.

The Core Team talked about this today, and we had an idea that we'd like one last bit of feedback on. We have decided to accept Sendable as the protocol name, borrowing the term of art sending from Rust for the act of sharing/moving values between different concurrency domains, which otherwise doesn't have a shortening that we believe to be acceptable. We were talking about what to call the function attribute — whether it should be something more "on the nose" for concurrency, like @concurrent, or whether it should align with the protocol name, like @sendable — when we realized that it was actually interesting to consider just using the protocol name, like @Sendable. This very neatly expresses the new capability introduced by the constraint, which is that the function value can be safely sent between concurrency domains, and thus that the function type conforms to Sendable. It also expresses the main restriction imposed by the constraint, which is that the captured values must conform to Sendable.

Grammatically, this would be a new use of user-defined attributes. For now, it would be limited to just the Sendable protocol, but in the future, we could allow this to be extended to apply to other protocols whose conformances can be autogenerated, such as Hashable or Codable. If Swift ever gains the ability to metaprogram a conformance to an arbitrary protocol, this could take advantage of that. We needn't design this feature right now; it's sufficient to see that this is a compelling future direction to take the language.

For these reasons, the Core Team is tentatively in favor of adopting the spelling @Sendable for the function type constraint in SE-0302. However, this is a novel idea which we haven't had much time to think about; more importantly, it's an idea that we haven't given the community an opportunity to think about. It would be inappropriate for us to immediately settle on it without discussion. Therefore, we are extending the period of this review until this Friday, March 12th, to allow the community to provide feedback on this new spelling.

All other aspects of SE-0302 will be accepted as proposed.

17 Likes

+1, this is what I suggested when the name was ConcurrentValue:

The purpose of @concurrent is to support function values that conform to ConcurrentValue and can therefore be used in contexts that require ConcurrentValue . The attribute-based design feels somewhat ad-hoc and not scalable. For example, we have discussed function values that could conform to Equatable (using equatable captures combined with source identity).

How would additional protocols fit into the current design? Would we introduce new attributes every time we want function values that have a new conformance (for example, an @equatable )?

I'm wondering if you have considered a more general approach. The immediate option that comes to mind is to use an @ConcurrentValue attribute on the function. This would allow @Equatable , @Hashable , and any other conformances that could make sense in the future.

7 Likes

Ah, sorry, I missed that in my write-up to the Core Team. You absolutely deserve credit there.

4 Likes

This is intriguing. As long as there isn't going to be ambiguity among the various kinds of user-defined attributes (which thus far include result builders, property wrappers, and now auto-generated conformances), I think this is very reasonable. It does very nicely settle any question that we want the function attribute to mean nothing more or less than conformance to the protocol.

Just to function types. @A @B (Int) -> Float means "the type of functions from Int to Float that conform to the protocols A and B". It is a different type from (Int) -> Float, although it is a natural subtype. This is a way of creating restricted function types, which is different from the potential ability to make unrestricted function types conform to protocols.

In general, supporting this for arbitrary protocols would require representation changes to function types; it does not for Sendable because it's just a marker protocol and can be fully enforced at compile time.

1 Like