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

Hello Swift community,

The second review of "sendable parameter and result values" (formerly "transferring isolation regions of parameter and result values") begins now and runs through May 13, 2024. The proposal is available here:

https://github.com/apple/swift-evolution/blob/main/proposals/0430-transferring-parameters-and-results.md

This proposal builds upon SE-0414 Region-based Isolation, which was accepted in February 2024. After the first review in March, the Language Steering Group accepted the proposal's design but asked the authors to select a keyword which better aligns with its role in concurrency.

Therefore, the scope of this second review is limited to the newly-chosen sendable keyword and potential alternative keywords. Given its limited scope, the review period will be relatively brief.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager by email. When emailing the review manager directly, please include "SE-0430" in the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

swift-evolution/process.md at main · apple/swift-evolution · GitHub

Happy reviewing,

—Becca Royal-Gordon
SE-0430 Review Manager

5 Likes

I think the name spendable perhaps limits this feature to just concurrency when there are broader use-cases related to ownership. From my understanding, most (if not all) of types we’d consider non Sendable have reference semantics. Thus, when we send a value across isolation domains, what we want to know is that the value in the new isolation won’t interact with any piece of data from the isolation domain we came from and thus race. With value semantics it’s guaranteed not to, so we have nothing else to check. For types with reference semantics, we basically have to guarantee that the instance on hand is unique (i.e. is only referenced once). While the other use cases for this feature are niche and likely limited, I can think of types that might need to take ownership of an object to fully manage it (perhaps even circumventing reference counting if they don’t reference it more that once).

So I guess my suggestion would be for something like noSideEffects or uniquelyReferenced.

Edit: I see that Rust has Unique<T> which is pretty similar. This feature could be implemented with the compiler automatically injecting that type:

// Definition 
@propertyWrapper
struct Unique<T: ~Copyable>: ~Copyable {
  private let value: T

  init(_ make: @autoclosure () -> T) {
    self.value = make()
  }

  var wrappedValue: T { … }
}

extension Unique: Sendable & Copyable where T: Sendable {}

// Usage
func acceptsUnique(@Unique x: some Any) {}

func sendUnique() {
  // The compiler could automatically add @Unique
  @Unique let x = NonSendable()

  // The compiler could automatically add the underscore
  acceptsUnique(_x)

  print(x) // Error: Wrapper _x already consumed
}

I feel pretty strongly that sendable doesn't quite fit here. I think it invokes a lot of the right stuff, i.e., that this modifier is 'concurrency-y', but I don't think it maps closely enough onto Sendable to make the overloading elucidating rather than confusing. I think it will be difficult to explain to those encountering Swift concurrency for the first time exactly what the difference is between sendable and Sendable.

I can tell myself a story that is perfectly coherent: a type which conforms to Sendable is one that is inherently 'sendable' in a broader sense, but individual values can still be sendable in this broader sense (namely, by being in a disconnected region). So we can mark a function parameter of a non-Sendable type T as sendable T to indicate that the particular value must be sendable by virtue of being in a disconnected region.

But this is subtle and I really don't like having keywords which mean different things based on capitalization. About the only exception is self vs. Self, which is interesting in that it makes the same value vs. type distinction, but there self really is a value of type Self. We would not have that property here: sendable is a modifier attached to a parameter type which nonetheless indicates something about the particular value which callees are allowed to pass.

I think if we choose sendable for this keyword it will make Swift's concurrency story harder to explain and talk about for no real benefit (over other available alternatives).

The proposal's rationale for sendable rather than sending (which remains my preferred spelling) is as follows:

It was also suggested that perhaps instead of renaming transferring to sendable , it should have been renamed to sending . This was rejected by the authors since it runs into the same problem as transferring namely that it is suggesting that the value is actively being moved to another isolation domain, when we are expressing a latent capability of the value.

But this doesn't strike me as particularly convincing. It is a higher bar than what we have generally held other parts of the function signature to. A throws isn't a function which always throws, it is a function which is permitted to throw. An async function doesn't necessarily suspend, yet nonetheless we mark the potential suspension point with await as though it always will. Externally, we treat the call as the suspension point even though the suspension may not occur precisely at the point of entry to the called function.

I view sending similarly. It is a description of the interface a function presents, which external clients must view as a single step. The function purports that it will be transferring the value across isolation domains, and indeed is permitted to, so external clients must act as though it always does, and moreover act as if the actual 'send' occurs at the point of the call. That the 'real' transfer may occur later (or not at all) is an implementation detail, immaterial to the caller.

22 Likes

That isn't quite what’s happening here. (If it was, we wouldn’t need a separate feature for this—noncopyable types already model unique ownership.)

Swift is basically enforcing that:

  1. The current function has the only external references to anything in the object graph.
  2. Any references into the object graph within the current function are never used after you “send” it somewhere else.

But the current function can have multiple references into that object graph—it’s just that when any of them are sent, all of them can’t be used anymore (so the function they sent it to knows that it now has the only external references). And there can also be references from one part of the object graph to another.

5 Likes

+1 on disliking sendable here. Sendable is already a "see an error I don't understand, but it mentions Sendable so I add : Sendable and it goes away" kind of a feature, which is concerning enough. Now we'd have sendable and Sendable, which mean different things, but sometimes you can use either to silence an error, and sometimes you can't?

I also don't think sendable helps me understand what happens when I add the modifier. SE-0414 already blurs what Sendable means (in that non-Sendable types can in fact end up being "sent"), and this sendable keyword just blurs the line further. It's extremely unintuitive to me that a non-Sendable type could be sent to a sendable parameter (and in fact, that's the point of the keyword), rather than that a sendable parameter could only accept Sendable types.

(and if you had trouble reading that paragraph, that's part of the point I'm trying to make!)

Whilst obviously this PR is necessary to make SE-0414 useful, and given the ship has sailed on SE-0414 I'm on board with the general concept of this one, I think what's really missing is a coherent story about what Sendable means now (given that it no longer means what it did when the name was chosen; perhaps we would choose UnconditionallySendable if we were starting over), and what it means to "send" something that's "not sendable" — that'll give us a name for the concept here (and probably make SE-0414 more understandable too)

5 Likes

We obviously need better documentation and other resources to teach these concepts.

But the relationship between the feature we're reviewing here and Sendable is actually pretty well captured exactly by what you mention here.

When a type conforms to a Fooable protocol, it's a guarantee that every value of that type can be fooed, but it isn't to the exclusion of other values (of non-conforming types) that may also be fooed. For a recent example, not every bitwise copyable value is of a type that conforms to BitwiseCopyable; but every value of a BitwiseCopyable type is bitwise copyable.

And so likewise, while every value of a Sendable type can be sent, also "non-Sendable types values can in fact end up being 'sent' ... (and in fact, that's the point of the keyword)." In fact we just adopted this feature in a mutex API proposed in SE-0433 precisely because transferring sendable was an appropriate relaxation of the originally proposed Sendable requirement.

We aren't confused by what SignedInteger is just because some values of floating-point types are also integers (which are signed), nor are we confused by what RandomAccessCollection is just because some string values can also support efficient random access (e.g., ASCII strings).

I'm not particularly married to sendable as the spelling for this feature, as opposed to (say) sending or sent, but I do vastly prefer something of that ilk, because it is precisely the point that we are describing the sending of a value across isolation regions.

The terminology we use to describe what the Fooable protocol guarantees to every value of a conforming type should be called "foo(-ing, -ed, -able)"; it doesn't make sense that for region isolation specifically we have to cast about for an alternative word so that Fooable guarantees that all values are "florpable" simply because non-Fooable values might be "florpable" too.

14 Likes

Thank you for your prompt reply!

That was kind of my point except there’s no current built-in Ownership feature that does what Unique<T> does.

Could you expand on what « external references » are and perhaps give an example?

Thank you for pointing this out, I hadn’t really thought about that. But is there a use case where having this is essential or even useful as compared to @Unique arguments as I showed above?

Stepping away from Sendable for the moment and using the example above of BitwiseCopyable, suppose we were making a new API to make a bitwise copy, we might start with:

func bitwiseCopy<T: BitwiseCopyable>(_ value: T) -> T { ... }

Now we do what this proposal does, and say that we want to extend this function to take values that the compiler can prove are bitwise copyable in that moment, as well. What syntax should we choose?

This proposal suggests,

func bitwiseCopy<T>(_ value: bitwiseCopyable T) -> T { ... }

Which, I guess makes perfect sense :sweat_smile:BitwiseCopyable refers to a type which always has the property, bitwiseCopyable refers to a value that currently has the property. I have to say, I'm not sure why I feel more strongly that sendable is bad, than that bitwiseCopyable would be.

An earlier commenter mentioned that Self and self have kind of a similar relationship, so it's not entirely unprecedented to have two keywords that differ only in case.

Still, please consider those of us trying to explain Swift concurrency over a video call or in a conference presentation! The difference between these is not easily pronounceable.

2 Likes

Absolutely. If you're familiar with Rust (I'm guessing you are, since you cited it), you're probably also familiar with the trouble its ownership model has with arbitrary object graphs where two objects can both point to each other; you often have to restructure your data to avoid doing that. This feature handles those situations a lot better.

Imagine you have an actor which loads some data from the network and constructs a bunch of objects from it, with all of those objects pointing to each other:

class CurrencyData {
    var currencies: [String: Currency] = [:]
        
    class Currency: Hashable {
        let root: CurrencyData
        let code: String
        var rates: [Currency: DecimalNumber] = [:]
    }
}

actor CurrencyDataLoader {
    func fetchCurrencyRates() async throws -> sendable CurrencyData {
        let rawData: [String: [String: DecimalNumber]] = try await fetchJSONData()
        
        let data = CurrencyData()
        
        // First pass: Create all currencies
        for (code, _) in rawData {
            data.currencies[code] = CurrencyData.Currency(root: data, code: code)
        }
        
        // Second pass: Populate rates
        for (thisCode, rateTable) in rawData {
             guard let thisCurrency = data.currencies[thisCode] else { ... }

             for (otherCode, rate) in rateTable {
                 guard let otherCurrency = data.currencies[otherCode] else { ... }
                 thisCurrency.rates[otherCurrency] = rate
             }
        }

        return data
    }
}

CurrencyData and CurrencyData.Currency are classes with unsynchronized shared mutable state, so they can't safely be made Sendable. And the data variable in fetchCurrencyRates() is not a unique reference—every single Currency has a reference to the exact same instance. But reading this code, we can easily see that only one outside reference to the CurrencyData object or any of its related Currency objects escapes the function—via the return value—and that therefore it really ought to be safe to let the CurrencyData be passed to a different concurrency domain.

SE-0414 and SE-0430 together make it possible for the compiler to prove that fact and allow this code to be compiled. You activate that by (in this case) marking the result type of fetchCurrencyRates() as sendable; this tells the compiler that the CurrencyData you return needs to be one that has this property of being provably reachable only by the caller of the method once it returns.

(If you'd like to know more about the details of the analysis, I strongly suggest you look at SE-0414 and the discussion around it; fully explaining it would get pretty far into the weeds for this review thread.)

Unique ownership is also a super useful feature in many scenarios, including some concurrency scenarios; that's why the ownership model (noncopyable types and related features) has gotten so much attention over the last couple years. But it isn't good at handling the cases that this feature handles, which is why we have both.

2 Likes

I came to say the same thing, but realised while reading the comments that actually sendable is the fundamental concept and making a type Sendable or a function @Sendable is just one way to do this. If these concepts were being explained from scratch, you would probably start with sendable and then go on to explain the Sendable protocol.

However, from the proposal:

The "sending parameter" behavior of actor initializers is a generally useful concept

This concept is introduced as ‘sending parameter’, so maybe that is a better name?

Or, would sends be a suitable alternative, along these lines? Just as a throws function doesn’t have to throw, it just has the option to do so, a sends parameter/ return value has the option to be sent?

5 Likes

Just going to make another pitch for reisolating as the keyword - I think it adequately conveys the meaning of what the modifier does, we’re already having to teach users about “isolation”, and it’s a case-insensitively unused name which will aid in searchability.

3 Likes

Proposal text seems to not cover how sendable keyword interacts with types known to be Sendable (or have I missed it?).

struct S: Sendable {}
func foo(_ x: sendable S) {} // ok, error or warning?

func bar<T>(_ x: sendable T) {}
bar(S()) // ok, error or warning?
3 Likes

+1 on vote against the sendable keyword. The possible confusion with Sendable protocol is too much here, and examples from the proposal IMO only support that it is complicated. The particular one that catches attention there is one of the most obvious uses:

func getNonSendable() -> sendable NonSendable {
    return NonSendable() // okay
}

Reads more like "make it Sendable", rather then moves it out of the region. Paired with region isolation it even more confusing:

func getNonSendable() -> sendable NonSendable {
    let ns = NonSendable() // initially sendable
    someActor.consume(ns)
    return ns // not okay
}

If you understand every part of that, it makes sense, but when you are trying to catch up with sendability in general it starts to fall apart. Because type that conforms to Sendable won’t produce an error in the example above, but sendable type does.


It’s too fundamental I’d say. Just mentioning all of that together in a conversation adds so much overload and friction to distinguish.


I would favour disconnected suggested in previous thread, as it makes a lot of sense in connection with regions, overall behaviour and zero conflict in naming with existing concepts before region based isolation. If not that one, transferring or any variation of it seemed OK too.

Under first edit at least it was stating that Sendable types are transferring (using previous terminology) as well, as marked with the new keyword. I think at most there might be a warning that this keyword is redundant in that case.

1 Like

I like the new sendable name:

  • The motivation under "Alternatives Considered" is strong.
  • I find the sample code easier to read and more intuitive since it reuses familiar concepts.
  • I like that it avoids introducing new terminology, especially complex terminology derived from region-based isolation (which is very advanced and beyond what I hope most Swift engineers will need to understand deeply).
1 Like

This feature, however it is spelled, will be a useful addition to the language that smooths over some of the rough edges of isolation domains.

I think, for someone new to Swift, having @Sendable, Sendable, and sendable in the language is confusing. "Why is it," a novice might ask, "that a closure/function argument uses @Sendable but any other type uses sendable?"

Keeping my novice hat on for a bit longer… I see this discussion of sendable vs. consuming:

When a call passes an argument to a sendable parameter, the caller cannot use the argument value again after the callee returns. [...] Unlike consuming parameters, sendable parameters do not have no-implicit-copying semantics.

And I wonder whether the distinction between sendable and consuming is semantically important at the source level? I (hat off for a second here) understand it affects codegen, at least as implemented today, but from an external developer's perspective, would it make sense to just use consuming here?

I'm unsure under what conditions I would want borrowing sendable. An example would be helpful.

sendable inout (but not inout sendable?) could also use some more explanation. I would assume that it means the value is transferred into the callee's domain for computation, then transferred back, but how would that differ from the normal semantics of inout? Could sendable simply be inferred here, or are there dangers in doing so?

10 Likes

I think the usefulness of the proposal is not in doubt at all so far (by anybody), but the disagreement of using sendable as a keyword in this way is strong. I am also someone that dislikes this much, as sendable NonSendable always seems to be in conflict.
Please also consider the mentioned issues about explaining this to novices. While there exist other, comparable contradictions in naming things in pretty much every language, including Swift, concurrency is a hard concept already and I think we should take care to try to alleviate any further complications in learning it.
Yes, even if you consider "learning" to be a one-time investment people have to do (which is debatable, too).

I tried looking at this from a more "general human language" perspective and I believe part of what irks us critics here is simply the fact we lack the needed abstracts in any regular pair of words. Sendable as protocol (and excuse me if I just toss its counterpart @Sendable in the same bucket) fits to "things being sendable" in the sense that we pass a "thing" over from one "place" (in this the languages isolation context) to another. The problems arise that in the real world, this usually means you can't do anything once you have sent it. Not so in our programming world, where it's normal that you can do stuff with things you have a reference to, whether they were also sent somewhere else or not. I wouldn't even say that was an oversight when choosing Sendable as a fitting protocol name, btw. The emphasis was simply on the sending aspect of this... process, not so much the "consequence" the word implies.

Now sendable refers to the same process, but this time the emphasis is basically put on "having lost the ability to work with it further".

I can see why the proposal does not want to use transferring as a keyword because it is not an action you apply to the parameter at the callsite, it is an adjective that tells that we must only use a reference that is sendable in the sense that we indeed no longer use it afterwards. Which is, agreed, an aspect of something that can be sent, but still: We have all learned that Sendable does specifically not mean that necessarily...

All that is to say: I don't believe having send in the keyword is nice. sending implies we actively do something with the parameter, which we don't, sendable gives huge friction to Sendable.

Since we kind of want to communicate that we "can give up on this parameter", what about relentable?

1 Like

That would indeed be in conflict. Fortunately, there is no type or protocol named NonSendable, and I would trust that no one would create such a type because, with region isolation, values of that type can indeed be sent. If someone does, then the fault lies with them.

1 Like

I did not not mean to imply NonSendable as a real protocol or type name, I was using it in the same way others have before, i.e. a type that is not conforming to Sendable (explicitly or implicitly). That's a little less terrible, but still not good imo.

3 Likes

It might be helpful to see how you feel with a concrete example and plausible (or real) API. For example, sketch out what it would be like to use the (actual) mutex API I mentioned above.