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

This feels similar to ~Copyable. The absence of Sendable conformance (in this case) doesn't actually mean anything specific - it just means the type is not categorically sendable. Specific instances of it could be. Calling a type NonSendable is just wrong unless there truly is some categorical reason why instances of that type can never be sent (in which case, it's a rather good name for it since it'll pre-empt the compiler errors that will otherwise result from trying to send a non-sendable thing). I'm not actually sure if that's possible.

While I still prefer sending over sendable because it's more consistent with other parameter modifiers, I think either will work in practice; they are explicable. I think the syntactic aspects are not going to be big problems in reality - it's not that hard to simply explain the difference between a whole type being categorically sendable (by conforming to Sendable) vs individual instances being sendable (by virtue of their usage patterns). It's the whole concept of "sending" and isolation in general that are the actual hard parts.

What I don't really like is that it's technically the wrong direction. The existing parameter modifiers are either neutral (e.g. inout) or from the callee's point of view (e.g. consuming). sending (the word) is from the caller's point of view. I worry that it will create some subtle confusion (even if only subconsciously). sendable does side-step that by being technically neutral in direction.

Can you have a sendable inout T parameter?

To provide some counter-balance to the thread: I like the sendable spelling.

I think the reasoning provided in the proposal is spot on, and in general this wording/way of thinking about what is expressed makes the a lot of sense in my mind.

Also, to me no other keyword that is floating around is really "clearly better" in terms of helping a newcomer explain what is going on.

If anything, one could argue the potential "confusion" around Sendable, sendable, and @Sendable is a feature. You should be confused, and you should be motivated to understand all these options when you are first confronted with the isolation/sendability/concurrency domain. No matter what the keyword is, you will need to learn about it anyway to be effective.

As a little side-rant:
I find the narrative - I am paraphrasing here - "but what about a novice that just want's compiler warnings to go away, they now will be confused which random keywords to throw a the code" a bit odd. I am all for progressive disclosure, but this ain't it. if you need to deal with isolation control, you will need to understand the language tools for it. No oh-so-cleverly named keyword will be able to take the inherent complexities of the domain off your shoulders.

3 Likes

I wanted to confirm that I understand correctly.

These parameters / return values will accept both:

  • A value of a type that conforms to ā€™Sendableā€™
  • A value that is in a disconnected isolation region

Is that correct?

2 Likes

i think what @Gero is getting at is that there are certain types in which the "non sendable" aspect is a major part of the education for how to use the type correctly. for example, Mongo.Session has documentation that relentlessly hammers the point that you cannot safely share it between concurrency domains. so when you see such a type, you're supposed to think "oh, that's some NonSendable thing".

i think between sendable and sending, the latter is a little less confusing.

4 Likes

I don't find this compelling. I think there's a good reason that Sendable was one of the first parts of the model that got put in place and that sendable hasn't been addressed until now. You can get quite far in your handling of concurrency issues dealing only with Sendable, and never worrying about sendable--in many cases region-based isolation will make things 'just work' and users can be blissfully unaware of the concept of isolation regions, which IMO are massively more complex to reason about than types. I really don't think we should want or expect users to have to encounter or learn about these concepts 'all at once'

Yeah, I don't think we're ever going to converge here on a pithy keyword that instantly makes it clear to a reader what's going on, and I don't think that should be our north star here. However, I also think that reusing sendable makes it more difficult for someone who is interested in understanding what's going on at a deeper level. Even just a search for 'sendable swift' would turn up many results which undoubtedly address the 'wrong kind' of 'sendable', without any indication to the user (other than the case of the first letter) that they're really reading about a different concept. At the very least, even if 'sending swift' turns up identical results, there would be a strong indication that they're not addressing the user's question head-on: it's a totally different word!

8 Likes

sendable inout parameters

A sendable parameter can also be marked as inout, meaning that the argument value must be in a disconnected region when passed to the function, and the parameter value must be in a disconnected region when the function returns. Inside the function, the sendable inout parameter can be merged with actor-isolated callees or further sent as long as the parameter is re-assigned a value in a disconnected region upon function exit.

1 Like

I get your point, absolutely.

However, on this specifically, I believe that it might do more "harm" than good: having developers plow through "blissfully unaware" - applying Sendable, actor, and global actor annotations on everything that moves - will ultimately result in a a lot of "bad code".

A first-class surfacing of non-sendable values flowing through isolation domains is in my opinion under-represented at the moment. Sure, if you don't know the screwdriver exists you end up not needing it - you'll be hammering in a lot of screws with the one hammer you have though.

1 Like

With consuming one gets a var like argument + no implicit copy diagnostics, while with sendable one just gets a var like argument. From a codegen perspective they are actually the same. The only difference is the additional diagnostic.

The reason why it is separated is that conceptually one may not want sendable to imply no implicit copy. One can always achieve that by marking the parameter consuming sendable.

borrowing sendable is mainly expected to be used in situations where one needs to change an API to use sendable, but one cannot change from +0 -> +1 due to ABI constraints. An example of this would be the Continuation.yield(with:) API in AsyncStream. The API takes a result at +0 so if we changed the parameter to be just sendable, we would be breaking ABI.

It will actually be inout sendable. That is just a typo in the proposal.

The difference is that an inout parameter only provides guarantees around the actual memory usage of the var. It doesn't provide any guarantees around concurrency. Consider the following:

class MyKlass {
}

@MainActor func sendToMain<T>(_ t: T) async { ... }

func f(_ x: inout MyKlass) async {
  await sendToMain(x)
}

func g() {
  var x = MyKlass()
  f(&x)
  print(x)
}

There is nothing in the current semantics of inout that would prevent this code from being written and we would have a race in between print(x) and whatever occurs in sendToMain(x).

The interesting use case here can be seen in the mutex proposal where it is used: [Pitch] Synchronous Mutual Exclusion Lock.

1 Like

That is a good point. I was also reflecting after my prior post that if we call this feature sendable, then the uppercase protocol might be more describing "sendable-able-ness."

In that case, and building on your argument that throws doesn't mean something always throws, I think it'd be fine not to be super fussy about parts-of-speech here and just spell this send T ā€” as in:

  // Actual line of code from mutex PR:
  public init(_ initialValue: send consuming Value) { ... }

[Addendum: I think from the perspective of the analogy it is fine to say that something is "sent" here. It is correct to say that you have "sent" a letter when you drop it in the mailbox, when in truth until the postal carrier picks it up it hasn't gone anywhere at all. You wouldn't correct someone and tell them that they've "isolated" their letter in the mailbox and made it "sendable."]

11 Likes

This is correct. One can always pass a Sendable typed value to a sendable parameter. I am imagining a diagnostic that in the case of concrete Sendable typed things or generic typed things that are constrained to be Sendable would get a diagnostic saying that sendable is redundant.

5 Likes

Yes.

3 Likes

I donā€™t disagree with overall behaviour. Yet the case of non-copyable generics I believe has made a point of the concept being hard to tackle. Here we have additional complexity of sendability concept, and to understand that keyword and type conformance with difference in one capital S has different impact and range of it, together with a bit of implicit behaviour of when instance is treated as sendable, and when it becomes not to, is a quite hard task, especially when you are new to all of that.

It is also not about naming of a type itself, but code snippet is expressive in how you can understand code.

And in terms of region based isolation, Sendable type and with this proposal now sendable type are in disconnected regions, which categorically Iā€™d say express state in less ambiguous way.

I actually read the mutex proposal, but before transferring was changed to sendable (at least in 0433 itself), so I did not notice it then. I actually thought it to be very reasonable and useful (even though I personally have no use-case for it atm), but did not feel confident enough in actually voting.

But I can gladly provide a small example, pretending to be a novice and not knowing what I know from this entire discussion: Let's assume I have some very simple reference type I want to protect with a mutex:

class PleaseProtecc: ~Copyable {
    // let's for a moment ignore that a novice probably won't get ~Copyable either... 
    var maybeARawFileHandle: Int32
}

Let's assume I am already using this elsewhere in my app and the strict concurrency diagnostics successfully prevented me from passing this around isolation regions because it does not conform Sendable. Perhaps I, although I am a novice, even know that whatever I do with it (let's say some socket I/O) is not "thread-safe", but I don't know exactly how I could make it thread-safe (because low-level C, buffers, whatever). I come across the Mutex API precisely because I am looking for something to protect my instance with.
But then, with autocompletion, I see:

public init(_ state: sendable consuming State)

Even if I just complete the snippet and do

let myMutex = Mutex(myPleaseProteccInstance)

and it doesn't show me an error or warning, I am most likely very hesitant that this is okay.
I might be under the impression that the sendable tells me that whatever I pass must conform to the Sendable protocol, which I tried to avoid having to conform to in the first place.

Now, taking the novice hat off, I know that this cannot be what is meant, as a required protocol conformance would be expressed via generics in the Mutex definition (which just requires State to be ~Copyable and not explicitly Sendable), but that's beside the point.

All in all, I am not completely against it, but a difference between this point of friction and others, language-wise, is that with Sendable we have taught people that the emphasis is explicitly not on the "you can't use stuff you have sent away", but sendable is precisely that.
That's different from, e.g. "not adopting BitwiseCopyable does not imply NotBitwiseCopyable problem. Here we're relying on two different aspects of the original (English) semantic of the word "sendable".

@taylorswift: Yes, that was one of my major issues.
And @sliemeobn: You are right, I am also not a fan of "over-protecting" any newcomers, complex things are complex and falling flat on your face with those once is actually often better in the long run, but in this case we should also not not regard it. We are using the same word.
I try to rephrase it better: "Sendable" in an English meaning would imply two things at once: A thing that can be "put from one place to another" (a form of sharing) AND, once that actually happens, "you cannot use it at the first place anymore" (relenting control).
Sendable focuses on the first part (we can still use a Sendable reference in the isolation region we "sent" it from), sendable on the second (we cannot).

4 Likes

When you "send" a letter, you no longer have the originalā€”but when you "send" an email, you very much have the original. Both letters and emails are small-s sendable; the action you take when you exercise that ability is called sending, and yet you relinquish only one but not the other. There is not a mountain of daily confusion about this, though, is there?

I could be cheeky here and point out that there's a lot of people who are very confused by email indeed, but just in jest... :smile:

Granted, perhaps I am overthinking this, but in our case we're not comparing two things quite as different as emails and letters, right? It's instances of types in both cases, and in both cases they're passed as parameters to a function. You'd be very surprised if you put two letters in two mailboxes, one of which has a "sendable letters" label and one just a "letters" label, and to your surprise, after putting them in, you'd still have a copy left in your hand of the one you put into the "letters" mailbox (yes, I kind of turned the example around here).

Oh, and since I am not sure: I hope it's clear I am all tongue-in-cheek here and not at all fighting for this to the teeth! I'd consider myself a small voice in this community and by far I am not of the opinion to be as qualified as many others in here!
To my surprise I also find myself much less conflicted about just using send (or sending), like in your edited post above. The problems with searching @Jumhyn mentioned would be addressed with this and are probably more important than my (non-native speaker) feel for the English language...

2 Likes

No, of course! This is key feedback and your voice is very important. We want to make sure that how we name things doesnā€™t make an advanced concept artificially more difficult to understand. Itā€™s good to see how far we can push explanations and analogies for clarity to get a sense of how this stuff is best to be documented and taught.

6 Likes

I think that's workable too, but sending still seems better as it's more in line with convention (similar parameter modifiers end with ing already). Ironically for people not fluent in English they probably don't care, whereas pedantic buggers like myself will be mentally tripping over the inconsistent tense forever. :laughing:

I think it's a good point that if you search for sending you'll almost certainly get results for Sendable, because current search engines are stupid, but if you're paying any real attention you'll quickly realise that you're not getting exactly what you asked for. Yet, what you are getting is probably going to be helpful anyway - whether because you didn't know about Sendable (but you should, if you're using sending, because they're merely different mechanisms for working on the same concept) or because the results talk about sending holistically, covering sending and Sendable and all the rest.

And, there's always quoting the search terms (or using a better search engine) to enforce a literal match. That's already something experienced programmers know to do anyway, because a lot of programming terms get mangled by default by popular search engines.

Hah, well, my grandparents would print emails before sending them in order to keep a copy for themselves, soā€¦ :stuck_out_tongue_winking_eye:

But, I concur that while presuming 'sending' unilaterally has reference semantics (basically) is not an implausible mistake for a learner to make, it's also not a big deal nor hard to correct. For better or worse Swift already has a complicated and sometimes obtuse system for value vs reference types, and I don't think anything in this proposal or naming discussion materially changes that.

1 Like

To elaborate, I like the subtle clarity from careful use of language:

  • Passive attributes are adjectives. e.g. the "able"s. Sendable, Codable, etc. Things that can have specific things done to them, or perform specific things, but don't imply anything about when those things occur - because that's up to how they're used, by other code (at least technically, and usually conceptually too).

  • Present progressive tense for parameters. e.g. "ing"s. Consuming, borrowing, sending. Things that apply only when the function in question is called (or property in question accessed, similarly).

    Seemingly these could have been past-tense just as easily, and arguably that would be easier to explain too - "when this function is called, this argument is sent and this one is borrowed" - but that ship has sailed.

    There are exceptions today, like "inout", which is a bit more adjectivy, but off the top of my head they're all not real words, which gives them some leeway. An alternative for "inout" would arguably be something like "mutating", or "replacing", or "returning" (whole other bike-shed there, not the point to discuss it here :sweat_smile:).

I can't think of an existing example of a past-tense keyword like these. I think past tense is likely to confuse since (a) it's different for no reason and (b) it could be interpreted as being a prerequisite for the parameter, not something that happens to it upon the call. e.g. "sent T" means some T that was previously sent [somewhere]. I dunno if that sounds convincing or contrived, but I think for "beginners" - which is most people using Swift today, in this context - we should be sympathetic to them misunderstanding these things.

6 Likes

As for alternatives, adopting was the conventional term in early C++ to indicate that the callee was keeping instead of copying the value.

Semantically, adopting takes the callee's perspective and has the requisite overtones of transfer, finality, and completeness (reachability). The implicit "parent/caretaker" is a bit weaker than "owner" but related in establishing a kind of context (like isolated), reinforcing its role in this ownership/async feature family.

But I might add that the perspectivism breaks down for me (for all proposed terms) when annotating not a parameter but the result.

Others have suggested evaluating this both from the perspective of experts and newbies. But I think the key perspective is how these kinds of features transit "Monkey see, monkey do": developers basically copy them and see how they work in practice, likely investigating fully only when it doesn't work as expected, after they've already invested in it.

The fault model there is establishing the wrong expectations; people get angry at being wrong. It's hard to change how you think about something, and you may feel mislead and become mistrustful.

To me the proximity of sendable and Sendable is likely to set the wrong expectation. Things that are close in meaning need distinctive terms to maintain separation. (E.g., imagine if internal and private were called invisible and hidden)

I appreciate Becca's emphasis on reachability in the object graph, which leads me in this case to focus on graph-completeness over sendability, and to prefer adopting.

2 Likes

I like adopting, although it feels more appropriate for reference types than value types. Usually when you adopt a child that means taking the specific child in question, not cloning them. :laughing:

1 Like