SE-0538: Disconnected

Hello, Swift community!

The review of SE-0538: Disconnected begins now and runs through July 31, 2026.

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 me as the review manager by email or DM. When contacting the review manager directly, please put "SE-0538" in the subject line.

Trying it out

Toolchains with the latest implementation are currently available for macOS and Linux. The Windows toolchain is still building on the implementation PR.

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 · swiftlang/swift-evolution · GitHub

Thank you,

Holly Borla
Review Manager

18 Likes

+1 makes sense. I've been using this type a bit myself as a primitive for implementing higher-level constructs, and it is useful as such (though often let down in current Swift by the lack of support for noncopyable types in other parts of the language and library).

My only nit is that I don't think take is the correct name for that API. We already have an API named take on Optional, which, importantly, leaves the container intact but empty. And I think we would consider adding e.g. takeAll() to RangeReplaceableCollection, consuming the contents but leaving an empty collection behind. This API does not do that.

I think we do need a naming scheme for "consume a noncopyable type and return the pieces it's conceptually composed of" — it does keep coming up as a natural design pattern. But perhaps consume or similar is the correct name for it, rather than take.

6 Likes

This type is going to be useful and I'm in favor of it.

During the pitch, I brought up a concern. The naming of this type moves the mechanisms of region-based isolation into the formal terminology that programmers need to use to understand the concurrency system. Not in a particularly aggressive way. You do not have to think hard about what "disconnected" means to use this. And moreover, this is a fairly advanced tool.

But, reading through the comment docs for the type, again, makes me wonder if there are alternative terms/concepts that could be used to model the same underlying behaviors. I want to stress that I'm concerned only about names and metaphors.

If this new terminology begins to show up in more places, especially diagnostics, I think we need to be absolutely sure this is appropriate for all levels of Swift user. I am, in particular, very skeptical that a distinction between "domains" and "regions" will be clarifying to people with less experience. I'm not certain there is a better approach. But it feels to me like it could be worth exploring.

10 Likes

Big +1 for adding a type like this, it seems very natural and useful. I can't envision RBI permitting the UniqueDeque case without a type like this.

I agree with @OneSadCookie that take may not be the best name, I like consume.[1] The doc comment for take even describes it as consuming the wrapper, which implies it would be an intuitive choice. It is a little longer though. consume is already a contextual keyword, but it describes the same operation as this method, so maybe that's ok? Potentially confusing. We should decide what we call the "remove and destroy" method on noncopyable types and be consistent.

I think this proposal's type name and mechanism forces us to decide how we feel about regions in the model of concurrency developers need to understand, so the rest of my review focuses on that. Maybe discussion of this should be its own thread, but I think we need to resolve it to commit to a name here.

I'm unconvinced that Disconnected is the right name. I agree with @mattie that we may be able to avoid talking about regions to have a type like this, and also feel that it could be valuable to do so (I'll come back to this). I think it is possible, although strained, to try to explain Disconnected only in terms of isolation; a type is "disconnected" when it doesn't have any references into or out of an isolation? Disconnected would be a state, rather than a kind of region. This isn't as cohesive with our other non-region concurrency language, but there is a path here that doesn't require naming regions. I think this is rather confusing to explain though.

I do not think that a concept being used in previous language evolution proposals is sufficient justification for it to be introduced as a developer facing type. There are lots of academic words (covariance!) we use in evolution documents that would make Swift feel unapproachable to developers. Today, we only surface 'region' in extremely narrow cases, and my understanding is that we intentionally do not name it in diagnostics and userdocs. SE-0414 did not suggest we would need to use these concepts in diagnostics or mental models:

NOTE: While this proposal contains rigorous details that enable the compiler to prove the absence of data races, programmers will not have to reason about regions at this level of detail. The compiler will allow transfers of non-Sendable values between isolation domains where it can prove they are safe and will emit diagnostics when it cannot at potential concurrent access points so that programmers don't have to reason through the data flow themselves.
SE-0414

Unless we are reframing concurrency in terms of regions, I would argue for Sent.[2] I don't agree with this rationale in the proposal against Sent:

sending describes a property of values at function boundaries (a transfer event), not a stable region state. A wrapper that simply holds a value living in a disconnected region is not mid-transfer, so naming the type after the transfer event would misrepresent what the wrapper is.

In my opinion, this is only an argument against naming the type Sending (gerund). Sent (past participle) is certainly a stable state, a thing has been sent. Even initializing this type requires sending the initial value, such that it "has been" sent to put it into the wrapper. It is being sent to wherever you pass it! This leaves us with a very coherent, linguistically grouped set of answers for "how to send this type" that do not require making regions developer facing:

  • Sendable types are always "able to be sent" and so can be sent freely!
  • a sending parameter or return value is contextually able to be sent, because it was made unavailable in the isolation it came from.
  • A value wrapped in Sent<T> has already been made unavailable in the isolation it came from, so it is able to be sent further.

Sent does not reflect the internal compiler understanding as well as Disconnected, but I think including it in this family might be worth it. If we choose Sent we could rename take to receive (as in, receive the sent value). I know disconnected is seen as a term of art, but I don't think we are required to use the most theoretically correct term in developer facing types if another choice makes the language easier to learn.

Coming back to why we should (maybe) avoid describing things in terms of regions; as @mattie says, concurrency is already really complicated! Even though regions are the internal model, we are not obligated to surface it, and if we can explain things with the terms we already have our language will be simpler to use and teach if we can show restraint here. If we can't explain things with the terms we have, then this would be very frustrating and we should reframe things in terms of regions. I see two decent futures (and a bad one) for the developer facing story here:

Everything is defined in terms of regions

Isolations are named regions based on annotations (global actor isolation) and types (actor instances) as well as scopes (tasks). However, there are also unnamed, flow sensitive regions that the compiler pessimistically determines to allow more code to compile. We would say Sendable allows a type to be shared between arbitrary regions (or maybe "isolation regions", but I feel this muddles everything). non-Sendable types can only be sent between regions if they are disconnected. Sometimes the compiler can prove values are disconnected and allow you to send them between regions, even though they aren't Sendable or sending. You can also wrap your value in Disconnected to ensure this is the case.

Everything is defined in terms of regions, so Disconnected makes sense.

We continue defining things in terms of isolation

An isolation protects its contents. Isolation comes from annotations (global actors), types (actor instances), and scopes (tasks). Sendable allows a type to be shared between arbitrary isolations. non-Sendable types can only be sent between isolations if they have no references / dependencies to or from other values in their current isolation, which the compiler will check for you. Once a non-Sendable value has been sent, it isn't available in its old isolation. You can use the sending annotation to require this to be the case for a parameter or a return value. You can also send a non-Sendable value into Sent<T> to lose access to it in the current isolation, but store it in a Sendable wrapper that you can take it out of in another isolation later. In this world, regions don't exist as far as the developer is concerned.

Developers don't see regions, so Disconnected is weirder than Sent in this world.

Both!

We could have isolation be the 'simple concurrency' and regions be the 'advanced concurrency' and not commit to a developer exposed relationship between the two of them. I'd prefer either of the above to this. It's easier to teach and understand a single, consistent story. We want a consistent way to talk about why sharing non-Sendable state is wrong, and sending it is only wrong sometimes. Whether we can talk about that in a satisfying way without regions isn't settled, but I'll try to steelman what diagnostics for that can look like below.

How could we explain RBI errors without saying 'region'

@Jon_Shier brought up a misleading diagnostic (ty!) in the pitch thread (where RBI pessimistically rejects code) that is roughly:

class NS { // something non-Sendable
    var count = 0 // 🚨 ahhh! shared mutable state!
}

func myScope() { // block of code, many more lines in actual programs (making these relationships less obvious to the developer)
    let one = NS()
    let two = NS() // two non-Sendable values, right now in separate regions

    someOperation(one, two) // compiler must merge those regions due to lack of type level information about 'someOperation', in case these values are mutated to reference each other

    Task { one.count += 1 } // the region containing 'one' and 'two' is sent into this task
    // the region containing 'one' and 'two' is no longer accessible

    two.count += 2 // compiler must diagnose this as illegal, since the region containing 'two' is no longer accessible
    //  ^ this mutation could also mutate 'one', which would be a data race
}

// compiler does not know based on the signature if these parameters mix / entangle / become dependent!
// it would prevent changing the body of functions if we inferred this quality, so we must assume the worst
func someOperation(_ left: NS, _ right: NS) {
    // what if left was mutated to hold a reference to right?
}

We get here because Swift doesn't statically enforce mutable xor aliased for reference types.[3] I agree that the diagnostic we give today is actively misleading for many reasons, but the part I think is most interesting; how do we justify why this is not allowed to the developer? The argument made in the pitch was that regions are already exposed since pessimistic merging is the reason this isn't accepted, and we need to expose it more to diagnose that well. We could speculate on a (maybe unrealistically?) "good" diagnostic that describes things in terms of regions, and even gives them temporary names:

Diagnostic that describes regions
func myScope() {
    let one = NS()
    //  ^ note: 'one' defined here in region 'a
    let two = NS()
    //  ^ note: 'two' defined here in region 'b

    someOperation(one, two)
    // ^ note: regions 'a and 'b must be merged into 'c after 'someOperation' since values in 'a and 'b may have become connected

    Task { one.count += 1 }
    // ^ note: sending 'one' into task-isolated region sends region 'c

    two.count += 2
    // ^ error: 'two' is in 'c which is no longer accessible
}

func someOperation(_ left: NS, _ right: NS) {
    // ^ note: 'someOperation' could mutate parameters to reference each other
}

This reads somewhat like a Rust lifetime error, because RBI is a system that pessimistically determines region membership / connectivity (potential to reference) via flow analysis (which is reminiscent of lifetime relationships).[4] But I don't think we need to talk about regions, or name what region these types are stored in to justify this error:

func myScope() {
    let one = NS()
    //  ^ note: 'one' defined here
    let two = NS()
    //  ^ note: 'two' defined here

    someOperation(one, two)
    // ^ note: 'one' and 'two' may reference each other after 'someOperation'

    Task { one.count += 1 }
    // ^ note: 'one' may reference or be referenced by 'two', and is sent into task-isolated scope here
    //   note: 'two' is sent here to prevent a data race

    two.count += 2
    // ^ error: 'two' cannot be accessed after being sent
}

func someOperation(_ left: NS, _ right: NS) {
    // ^ note: 'someOperation' could mutate parameters to reference each other
}

This diagnostic could use some new term (dependency? entangled?) to be more terse, or frame the problem differently, but my point is that we can explain the issue without reformulating the developer facing concurrency model in terms of regions. RBI is "just" the internal mechanism the compiler uses to accept additional, provably race free code. If we can explain the errors that RBI leads to (since there are cases which are simply unsafe and must be diagnosed) clearly, and in ways that help developers understand how to fix them, without invoking regions, I think we should do that and avoid naming any types in ways that only make sense in terms of regions. Such diagnostics still make sense when read with knowledge of regions (potential reference forces merging), but don't require understanding them.

Note that this diagnostic does introduce a new idea, this concept of "potential to reference", to the concurrency system (this is part of the definition of regions from SE-0414). I'd argue the problem with developer facing regions is explaining how they are different from isolation, which is which and when. Programmers already think about references / dependencies and when they are safe (like for avoiding retain cycles). You could say this renames regions as a friendlier thing--but I think that's fine, and potentially how we can square the need to diagnose these cases clearly with a model that is easy to teach. We will need language for why "regions" are merged, but maybe it isn't regions...

Having said all that, I'm really open to the possibility that surfacing regions is the way forward on these issues; I just want to make sure we consider that thoroughly first.

Speculative musing about lifetimes and RBI annotations

What if we had region annotations? I'd like to briefly highlight that, since regions and lifetimes talk about similar questions (do these values share dependency) from different perspectives, they share a deep correspondence and we may be able to express any interesting RBI annotations as lifetime dependencies (if we allow them on copyable types):

@_lifetime(left: copy left, right: copy right) // left and right will not alias! this can't actually be expressed today, and doesn't make sense on the values we allow lifetimes on right now.
func someOperation(_ left: NS, _ right: NS) {
  // left and right will not depend on each other, since signature promises no reference is taken
}

In a sentence, lifetime dependency implies RBI aliasing / referencing potential, but mutual lifetime independence requires non-aliasing / non-referencing.

Putting the non-aliasing / non-referencing 'promise' in the type system allows RBI to avoid merging the two regions. I think it would probably be bad for progressive disclosure to tie regions and lifetimes, but my point is that even if you think we want RBI annotations, we may be able to express them as dependencies (lifetimes are just how we add dependency relation to function and data types in Swift) and avoid complicating concurrency with the concept of regions.


  1. I thought about connect and derivatives but I think it's more confusing than clarifying. ↩︎

  2. Suggested by @pyrtsa in the pitch thread, ty! ↩︎

  3. A very fancy RBI could determine that NS can't store a reference to another NS or any of its potential fields with reference semantics, but ignore that (or consider NS to be a library type) ↩︎

  4. I also just like lifetimes. ↩︎

10 Likes

+1 to everything Aviva said! To add on a couple of things:

If the type is called Sent, then how about calling the take method send()?

var deque = UniqueDeque<Sent<NonSendable>>()
deque.append(Sent(NonSendable()))
guard let element = deque.popFirst()?.send() else { return }
Task { print(element) }

I’m not fully convinced that this is a clearer term than consume() but it fits in a little better with the fact that the return type is sending Foo.

And I think the error messages you proposed would be extremely helpful in debugging RBI/sendability issues — today, I am very often stumped because some random piece of code has an error with no clear justification.

Speaking for myself, I like the name Disconnected. I don’t think you need to know region analysis in order to get this; disconnectedness is fundamentally just a statement about the object graph, which is both pretty intuitive and very easily visualized. (It’s also something we already expect programmers working with classes to have a handle on, e.g. because of reference cycles.

7 Likes

I think send is a little confusing, since the value is already sent (that's how it got inside the container). You could call the initializer send, but we don't really need to label this initializer. The return type being sending is (afaik) reiterating to the compiler that it's still in a disconnected region and can be sent across isolation boundaries.

If Sent, my personal opinion is receive or consume [1] > take.
If Disconnected, consume > take.

I agree that "disconnected is a state a value can have within the object graph" is a concept a programmer could understand without regions (my 'may reference' diagnostic example is describing the lack of that state). Right now though, the proposal has the doc comment below teaching it via regions, so it's not neutral on Disconnected (isolation region) vs Disconnected (a state in the object graph). I'm being pedantic,[2] but I think those are different things for how we teach concurrency even if we choose Disconnected.

/// A wrapper that holds a value in a disconnected isolation region.
///
/// A value of `Disconnected<Value>` lives in a disconnected region: it has no
/// references to or from any other isolation region. That guarantee lets you
/// store such values in generic containers and later transfer them across
/// isolation boundaries without losing the information that the value was
/// in a disconnected region.
///
/// ## What is a disconnected region?
/// ...

The detailed design has the full doc comment at the top, with an (in my opinion) great explanation of regions and how this fits into them. The implication though is that you do need to know about region analysis to understand this type, at least as part of this proposal.

@John_McCall, do you think the term of art value / conceptual value outweighs the linguistic grouping value? What about for a type that isn't being taught in terms of regions?


  1. contingent on us feeling like this is the word we want to use for this operation on noncopyable types... ↩︎

  2. since RBI regions are regions in the object graph ↩︎

Documentation is certainly something we could just change. I would not want this to be taught using regions.

I'm happy to acknowledge that there's value in sticking to some kind of "sending"-related name. I don't think Sent<> is a good choice, though. The statement that the value was sent in the past doesn't seem to capture either what happened to it or how it can continue to be used. If anything, the value is now in the same currently-in-transit state as sending parameters and results and ought to have the same name. Sending<> is probably the best option in this vein if we can come to terms with it just differing from the keyword by case.

6 Likes

Beyond being clearer, Sending<> is also shorter and more consistent with the existing sending annotation already used in function signatures. This alignment between the type and the parameter/result annotation helps build a natural and intuitive mental model: values flow through sending boundaries, and Sending<T> is simply the container that carries them across.

I've recently needed to use Disconnected in typed throw contexts where the closure signature must be written explicitly, for example in closures passed to functions like withUnsafeMutablePointer(to:):

func foo<Result, E>(...) throws(E) -> sending Result { 
  ... = bar() { argName throws(E) -> (Bool, Sending<Result>) in
    // ...
  }
}

Sending<Result> would be much more understandable. The naming makes the flow obvious: you receive a Sending<Result>, consume its value and return a sending Result.

The mental mapping from Sending<Result> to sending Result is intuitive and reinforces Swift's existing concurrency vocabulary.

I agree with others that a type of this general form has utility, particularly for enabling the sending annotation to be preserved through storage within containers and properties.


I also agree that take() doesn't seem like quite the right name, given the proposed semantics and how Optional.take() works. The UniqueBox evolution proposal occurred fairly recently and chose consume() with this justification, which seemingly also applies here.


swap(newValue:) also reads a bit oddly to me.

The type is a "simple" wrapper that has a single stored value, so any mutating operation presumably affects that value in some way. Given that, the argument label "newValue" is arguably overly verbose. If distinguishing that the underlying "value" is the thing being "swapped" seems valuable, then perhaps that should be pulled out into the function name. These alternatives seem like improvements to me:

var d = Disconnected(...)
d.swap(with: x)
d.swap(to: x)
d.swapValue(with: x)
d.swapValue(to: x)

But, stepping back a bit, the naming choice of "swap" also seems a bit questionable... the prior art with which I'm familiar in Swift are the global function swap(_:_:) and the collection-based swapAt(_:_:). Both of these take two explicit arguments of the same type and exchange them (or the underlying data to which they correspond), which doesn't seem perfectly analogous to what's going on here (semantically we're replacing a stored value within a container and returning the old value). The stdlib has an exchange(_:with:) function which has a slightly closer shape. Perhaps something involving the word "exchange" or "replace" would be better (it appears Rust has a "replace")?

var d = Disconnected(...)
d.exchange(with: x)
d.exchangeValue(with: x)
d.replace(with: x)
d.replaceValue(with: x)

To summarize, writing all this out makes me feel there are two primary questions about the proposed naming choices for this method:

  1. Should the word "value" appear in the function name, or for basic "box" types such as this, is the language direction to rely on argument labels instead.
  2. Given precedent, should a term other than "swap" be chosen.

Disconnected as the type's name seems okay, but I'm sympathetic to concerns that it surfaces implementation jargon that may not be particularly helpful or explained well in any extant documentation (though the proposed comments in the implementation PR do seem quite good!).

Personally I think Swift already suffers a bit from having so many terms involving negations (non-copyable, non-escapable, non-sendable, non-isolated...). This pushes you to first understand whatever the "core" terminology is, and then negate it in some manner. In this case that's sort of more confusing than usual because the implied state of "connected-ness"[1] isn't really a formalized notion that's directly exposed in the language AFAIK. That said, I haven't yet thought of a clearly better alternative, and the fact the term arises in graph theory and sort of conveys the right general connotation is a point in its favor.

Sent<T> seems worse to me, mostly because it does not align with my mental model of what the state of being "sent" means in region analysis. "Sent" to me implies that the one-shot ability to transfer a non-sendable value between isolation domains has already happened. But with the proposed API, placing a value within a Sent<T> wrapper does not semantically "send" it in that sense and so the value is not "sent" IMO. The property we're preserving is some latent ability to be sent at some future point. The natural-seeming name for something like that would, I think, be "sendable", but obviously that one is out of the picture. Perhaps we could retain something like "sendable" but express that it's a one-time-only sort of variation... "UniqueSendable<T>", "SendOnce<T>", or something along those lines?


I don't entirely understand the motivation for housing the type in the synchronization module rather than in the concurrency module. Personally that's not where I would expect to find it if I hadn't read any of this discussion.


Using a non-copyable type here seems it may be necessary, but this also means the type will suffer from some usability issues IMO. For instance, consider a variation of the motivating example – we have a deque and we want to process all its elements in parallel, in some way which requires the elements to retain their sending-ness. We wish to use structured concurrency to simplify our lives. We might try to write something like this:

open class NS {}

func useSending<T>(_: sending T) {}

func drain(_ dq: inout UniqueDeque<Disconnected<NS>>) async {
    await withTaskGroup { grp in
        while let next = dq.popFirst() {
            grp.addTask { useSending(next.take()) } // 🛑
                                    // error: noncopyable 'next' cannot be consumed when captured by an escaping closure or borrowed by a non-Escapable type
        }
    }
}

That doesn't work because the non-copyable types can't be consumed in an escaping closure, so we try another variant:

// Okay, maybe we need to take the values out of the wrapper before
// using them in the child tasks?
func drain2(_ dq: inout UniqueDeque<Disconnected<NS>>) async {
    await withTaskGroup { grp in
        while let next = dq.popFirst() {
            let val = next.take()
            grp.addTask { useSending(val) } // 🛑
                        // error: sending 'val' risks causing data races
        }
    }
}

But that approach seems to lose the property that Disconnected is designed to preserve. With sufficient fiddling, we find that the following alternatives will work:

// I guess a mutable binding works... but what if we can't reconstruct a new value?
func drain3(_ dq: inout UniqueDeque<Disconnected<NS>>) async {
    await withTaskGroup { grp in
        while var next = dq.popFirst() {
            grp.addTask { useSending(next.swap(newValue: NS())) } // ✅
        }
    }
}

// I suppose I could... write my own box?
func drainCustomBox(_ dq: inout UniqueDeque<Disconnected<NS>>) async {
    final class SendOnce<T> {
        private var v: Disconnected<T>?
        init(_ t: consuming sending T) { self.v = .init(t) }
        func consume() -> sending T { v.take()!.take() }
    }

    await withTaskGroup { grp in
        while let next = dq.popFirst() {
            let myBox = SendOnce(next.take())
            grp.addTask { useSending(myBox.consume()) } // ✅
        }
    }
}

But at that point... will people have just given up and opted out instead?

// That seems pretty convoluted... I guess I could just opt-out.
func resignedDrain(_ dq: inout UniqueDeque<Disconnected<NS>>) async {
    await withTaskGroup { grp in
        while let next = dq.popFirst() {
            nonisolated(unsafe) let val = next.take()
            grp.addTask { useSending(val) } // ✅
        }
    }
}

I'm not sure if there's actionable design feedback to be drawn from this, but I am curious if there are ways to make things work more nicely when escaping closures are involved.


  1. Or should we say "non-disconnected-ness" :upside_down_face: ↩︎

3 Likes

This is the primary motivation behind the swap method. As things stand, we can't use a consuming method in such cases yet. I do wonder whether, if we renamed take to consume(), for instance, we could make take available only when Value is optional (or should take always be available?) and make it a mutating method that replaces the value with nil. For example, we did something similar here.

I like the Sending<> name John suggested. I found Sent a bit confusing, for a similar reason to what John mentioned.

I think receive works great with the metaphor.

PS: I like the original Disconnected name too. Is it really a goal to not expose isolation region to users? I'm quite sure that the term "task isolated" appears in some diagnostics.

1 Like

Not anymore: [rbi] Replace "task-isolated" with "code in the current task" in diagnostics by gottesmm · Pull Request #89552 · swiftlang/swift · GitHub. I guess this is another reason for Sending<Value>.

2 Likes

In natural language, "sending" is an attribute of the thing that's doing the sending, rather than of the thing being sent, unlike "disconnected" which could reasonably applied to either, though perhaps with a slight change in connotation.

The sending keyword works, barely, in function parameters because it's the function call — or perhaps the caller's region — that's doing the sending. It's not creating a "sending" value, and IIUC it's not even creating a Sending<> value, if SE-0538 were accepted with that terminology, is it?

IIUC, the "disconnected" concept presented in SE-0538 is more like transiting than sending. However, I don't think anyone would be happy about choosing a spelling-unrelated word such as "transiting", or anything else I could think of. (That would put this back in inout vs borrowing territory.)

Given that disconnected values and disconnected regions could co-exist in the Swift Conceptual Universe without contradiction, but "sending" values embody a sort of self-contradiction, I'd advocate sticking with Disconnected<>, FWIW.

Incidentally, although "disconnected" is very much the computer science/graph theory word for this sort of thing, "unconnected" is perhaps the better English word for the idea of not-being-connected. If my Mac gets disconnected from a network, it's generally a different thing than my Mac being unconnected.

2 Likes

Sending<> also has the benefit of matching Inout<>, which already differs from its corresponding keyword by case. as discussed below it was renamed MutableRef

1 Like

I like Sending or Sent. Disconnected feels… disconnected from the existing terminology.

+1 to the overall feature.

Edit:

@Sendable and sending differ in case already, as do actor and Actor, class and AnyObject… I think there's plenty other prior art there.

1 Like

We did not accept Inout<>; it's MutableRef<>.

1 Like

I could also envision this being called SendingCell because the take/swap semantics are precisely the same as offered by Rust's Cell — and there's a _Cell in the stdlib already underpinning the Mutex implementation, which I could imagine becomes de-underscored at some point with similar semantics as well.

Good addition! I have no particular opinion about the type name, but I do about swap and take.

UniqueBox has established consume() as the name for exactly that operation, and it applies better that take(), a name we already use for a mutating operation of Optional, rather than a consuming one.

As for swap, the shape of other functions of that name in the standard library is swap(&a, &b) -> Void; the better analog is the top-level exchange<T>(_: inout T, with: consuming T) -> T. We didn't need such a function for UniqueBox because it can provide a mutable accessor to its stored value (usable with the top-level function). exchange could also be called replace, but we already have exchange.

3 Likes

I find a corner case where Sending is more accurate than Disconnected. First, let's see SE-0430 (sending parameter and result values). It chose sending rather disconnected. That makes sense to me because a Sendable value can be passed to a sending parameter and the value doesn't need to be in disconnected region. I think the same applies in this case. While the proposed type isn't intended to wrap a Sendable value, there is nothing in its implementation preventing user from doing that. Since the type's init takes a consuming sending parameter, it's fine to pass a Sendable class instance to it and continue to access that instance in the original isolation. In this specific scenario Disconnected is an inacurrate name.

EDIT: I should add that while many APIs changes their parameters from Sendable to sending after SE-0430, it's unlikely to happen in this case. So the two cases are similar but not exactly the same.