[Pitch] `Disconnected` type for modeling disconnected values

Hi everyone,

I have written a Disconnected type for modeling the concept of a disconnected value in the type system. This type has been copied around into multiple places now, such as swift-async-algorithms, and has proven to be helpful when implementing algorithms and abstractions that need to work with disconnected values. In particular, when the values need to be stored in container types like UniqueArrays. I would love to hear your feedback on this type. Below is the full pitch copied from Propose a `Disconnected` type by FranzBusch · Pull Request #3300 · swiftlang/swift-evolution · GitHub.


Introduction

SE-0414 introduced region-based isolation which leverages control flow sensitive diagnostics to determine whether non-Sendable values are safe to send across isolation boundaries. SE-0430 introduced the sending parameter and result annotation to explicitly mark values that must be in a disconnected region at function boundaries.

This proposal introduces a Disconnected type that preserves the disconnected property of a value through storage in data structures, allowing generic types to safely transfer non-Sendable values across isolation regions without requiring those types to reason about the sending effect.

Motivation

Region-based isolation enables transferring non-Sendable values across isolation boundaries when the value is in a disconnected region. The sending parameter and result annotation from SE-0430 allows functions to explicitly require disconnected values at function boundaries. However, sending cannot be preserved through stored properties, collection types, or generic containers.

Consider a queue implementation that stores elements to be processed across isolation boundaries. As an example, let's look at a hypothetical UniqueDeque:

struct UniqueDeque<Element: ~Copyable>: ~Copyable {
  func append(_ element: consuming Element) { ... }
  func popFirst() -> Element? { ... }
}

One use-case might want to use the UniqueDeque to append non-Sendable disconnected values and when popping an element send it to a different isolation region.

var deque = UniqueDeque<NonSendable>()
deque.append(NonSendable())

guard let element = deque.popFirst() else { return }

Task {
    print(element) // Error: Element is assumed to be in the same isolation region as uniqueDeque
}

To make this work we would need to consume the element in append and return it from popFirst as sending; however, this would significantly limit this type for other important use-cases where users want to store non-Sendable but not disconnected elements.

The fundamental limitation is that sending is a property of function boundaries, not types. Generic types like UniqueDeque cannot conditionally apply region isolation based on whether their element type should maintain disconnected regions. Making append and popFirst use sending would prevent legitimate use cases where elements should remain in the same region.

Proposed solution

This proposal introduces a new Disconnected type that allows us to model a disconnected value.

var deque = UniqueDeque<Disconnected<NonSendable>>()
deque.append(Disconnected(NonSendable()))

guard let disconnected = deque.popFirst() else { return }

Task {
    let element = disconnected.take()
    print(element)
}

The Disconnected type wraps a value, ensuring it remains in a disconnected region. The take() method consumes the Disconnected wrapper and returns the value as sending, allowing it to cross isolation boundaries.

Detailed design

The Disconnected type is a simple wrapper that enforces region isolation through the type system:

/// A type that wraps a value in a disconnected isolation region.
///
/// Values of type `Disconnected<T>` are guaranteed to be in a disconnected
/// region, meaning they have no references to or from other isolation regions.
/// This allows them to be safely transferred across isolation boundaries and
/// stored in data structures that preserve the disconnected property.
@frozen
public struct Disconnected<Value: ~Copyable>: ~Copyable, Sendable {
    /// Initializes a new disconnected value by consuming the passed value.
    ///
    /// The value must be in a disconnected region. This is enforced by
    /// requiring the parameter to be `sending`.
    ///
    /// - Parameter value: The value to wrap in a disconnected region.
    public init(_ value: consuming sending Value)

    /// Provides borrowing access to the wrapped value without consuming the
    /// wrapper.
    ///
    /// Because this is a `borrow` accessor, the wrapped value cannot be
    /// mutated or replaced through it, preserving the disconnected region
    /// property of the wrapper.
    public var value: Value { borrow }

    /// Consumes the disconnected wrapper and returns the underlying value.
    ///
    /// The returned value is `sending`, indicating it is in a disconnected
    /// region and can be transferred across isolation boundaries.
    ///
    /// - Returns: The wrapped value as a `sending` result.
    public consuming func take() -> sending Value

    /// Swaps the current disconnected value with a new one.
    ///
    /// The returned value is `sending`, indicating it is in a disconnected
    /// region and can be transferred across isolation boundaries.
    ///
    /// - Parameter newValue: The new value to wrap in a disconnected region.
    mutating func swap(newValue: consuming sending Value) -> sending Value
}

The Disconnected type conforms to Sendable because it guarantees its wrapped value is in a disconnected region. Since disconnected regions can be safely transferred across isolation boundaries, Disconnected<T> is safe to share regardless of whether T conforms to Sendable. The value borrow accessor is sound because it cannot mutate or replace the wrapped value, so the disconnection invariant is preserved for the duration of the borrow. Furthermore, all mutating methods on Disconnected are either consuming or mutating which means that the compiler will enforce static and dynamic exclusivity checking prohibiting overlapping and concurrent access.

This shape also composes naturally with the borrowing accessors on generic containers introduced by SE-0519. A container holding Disconnected<Value> elements can expose a Ref<Element> projection without any knowledge of Disconnected, and callers can drill through to the wrapped value via the value accessor.

Source compatibility

This proposal adds a new type to the standard library. No existing code is affected.

ABI compatibility

This proposal adds a new @frozen type to the standard library. The layout of Disconnected is ABI stable. No existing ABI is affected.

Implications on adoption

The additions described in this proposal require a new version of the Swift standard library and runtime.

Alternatives considered

Alternative names

Different names such as Nonisolated and DisconnectedRegion were considered; however, the name Disconnected felt the most fitting. Furthermore, the concept of a disconnected region was introduced in previous proposals.

Using sending annotations on generic parameters

Rather than introducing a wrapper type, we could attempt to parameterize generic types over whether their elements are sending. This would require significant language changes to support conditional application of sending based on generic constraints, and would complicate generic type signatures. The wrapper type approach provides equivalent functionality with no language changes beyond the library addition.

Making Disconnected a protocol

A Disconnected protocol could be applied to existing types. However, this would require proving that all values of conforming types are in disconnected regions, which cannot be enforced for mutable types. The wrapper type approach provides stronger guarantees by construction.

Exposing Ref and MutableRef projections

Rather than (or in addition to) the value borrow accessor, Disconnected could expose dedicated projections producing the reference types from SE-0519:

extension Disconnected where Value: ~Copyable {
  public var ref: Ref<Value> { borrow }
  public var mutableRef: MutableRef<Value> { mutate }   // unsound, see below
}

A ref: Ref<Value> projection would be sound for the same reason the value borrow accessor is sound: Ref.value is itself a borrow accessor, and Ref<Value> is Sendable only when Value is Sendable, so a Ref<NonSendable> cannot be exfiltrated to another isolation region. However, it is redundant: callers who want a Ref can construct one explicitly from the value accessor, and generic containers built on SE-0519 will naturally produce Ref<Disconnected<Value>> without Disconnected needing to participate. Adding a dedicated ref property would duplicate the existing borrow accessor without enabling anything new.

A mutableRef: MutableRef<Value> projection, by contrast, would be unsound. Disconnected: Sendable is unconditional, which means the type system trusts the wrapper to keep its contents in a disconnected region. The setter on MutableRef.value accepts any Value in the current region without a sending constraint, so it would allow code like:

var disconnected = Disconnected(NonSendable())
disconnected.mutableRef.value = nonDisconnectedValue   // silently merges regions
// disconnected.take() now hands out a "sending" value that isn't disconnected

Mutating methods reached through mutableRef.value could capture references into other regions in the same way. The existing swap method covers the sound version of "replace the wrapped value with a new one" by requiring sending for the replacement, and is the only mutating projection that can preserve the disconnection invariant without language-level support for sending-constrained mutation.

Support for ~Escapable values

The current design restricts Disconnected to escapable types. The disconnected region property is conceptually independent of lifetime dependencies, so it is
tempting to relax the Value constraint and make Disconnected conditionally Escapable:

struct Disconnected<Value: ~Copyable & ~Escapable>: ~Copyable, ~Escapable, Sendable { ... }
extension Disconnected: Escapable where Value: Escapable {}

However, this generalization is not useful in practice. Nonescapable types as
introduced by
SE-0446 are non-owning views with a lifetime dependency on some source storage (e.g.
MutableSpan<Element> borrows from an Array<Element>). This creates two
problems:

  1. No sending form exists at the source. View types are produced by
    borrowing accessors that return a value with a lifetime dependency on self.
    There is no sending accessor to consume, so
    Disconnected(array.mutableSpan) cannot even be constructed.
  2. The lifetime source does not travel with the wrapper. Even if a sending
    view could be produced, the view still carries a reference into storage that
    lives elsewhere. Transferring Disconnected<MutableSpan<Int>> to another
    isolation region leaves the backing Array behind, violating the
    disconnected region property by construction. A generic wrapper has no way to
    know what the lifetime source is or to carry it along.
19 Likes

Isn’t var value a sendability hole? If you have a struct Foo { let nonSendable: NonSendable } you could access disconnected.value.nonSendable in different isolation regions, or would the compiler catch this?

3 Likes

I just added the value accessor after having thought about it for an extended time and convincing myself that it is safe, but after re-thinking and using your example, I think you are right, and this accessor is not safe.

1 Like

You could probably do withValue<T>(_ body: (inout sending Value) -> T) safely, but not sure that buys you much over just taking the value and replacing it.

Also, in my own project I’ve been using send and receive for symmetry with sending

Yeah, I think any access to value, whether read-only or mutating, would need to happen under exclusive access to the Disconnected, and maintain the disconnectedness of the value until the access ends, so it seems like it would have to behave as if you used take() to grab the value at the beginning of access, then put it back in a new Disconnected when you're done. If we allowed something like this (with the treatment that a borrow of a sending Value must not break the value's isolation), it might work:

  var value: sending Value {
    mutating yielding borrow {
        let value = take()
        yield value
        self = .init(value)
    }
    yielding mutate {
        let value = take()
        yield &value
        self = .init(value)
    }
  }
4 Likes

For generic code that needs to use Disconnected for generality, but might still be used with Sendable concrete types, it could also be useful to provide a conditional extension to give unfettered direct access to the underlying value when it is concretely Sendable:

extension Disconnected where Value: Sendable {
  public var sendableValue: Value { borrow; mutate }
}
2 Likes

Another hole for value, for anyone trying to restore the original form: existingObjectGraph.importantProperty = disconnectedObject.value. Region isolation is really hard to re-establish once you've opened it in almost any way!

1 Like

Without having given this proposal much thought yet, I’m quite strongly against formalizing any sort of Disconnected type, as I don’t believe this is the idiomatic solution to this limitation. I would also assume that doing so would diminish the motivation to resolve this in a future RBI evolution. I think the correct approach is to continue improving RBI itself, even if that’s a tall task.

I find myself writing a lot of Disconnected types as well, though usually in slightly different variations with “unsafe” initializers and crashing take methods, since I find using sending on init and consuming on take to be quite limiting. To me, the existence of Disconnected types is purely a workaround.

1 Like

This proposal is not intended to prohibit any future improvements for RBI. While there are some existing usages of this type that should be handled by other language features, in particular, the lack of call once closures is often resulting in the usage of Disconnected, there are usages that I don’t think RBI will ever be able to solve such as storing disconnected values in containers. The only other way that I could see this being solved is called out in the alternatives considered section by introducing some kind of effect polymorphism around sending.

1 Like

I read this and the API signatures, and it occurred to me that a more familiar name from the already existing API vocabulary for a value to be sent across regions could be Sent<Value>.

1 Like

I've been using this definition:

public struct Disconnected<Value: ~Copyable>: ~Copyable, @unchecked Sendable { // swiftlint:disable:this unchecked_sendable
    private nonisolated(unsafe) var value: Value

    public init(_ value: consuming sending Value) {
        self.value = value
    }

#if compiler(>=6.2)
    /// Swift 6.2 may miscompile `consume()` when inlined.
    @inline(never)
#endif
    public consuming func consume() -> sending Value {
        value
    }

    public mutating func swap(_ other: consuming sending Value) -> sending Value {
        let result = self.value
        self = Disconnected(other)
        return result
    }

#if compiler(>=6.2)
    // I don't think SendableMetatype should actually be required here.
    // https://github.com/swiftlang/swift/issues/83253
    public mutating func take() -> sending Value where Value: ExpressibleByNilLiteral & SendableMetatype {
        swap(nil)
    }
#else
    public mutating func take() -> sending Value where Value: ExpressibleByNilLiteral {
        swap(nil)
    }
#endif

    // Unsound until https://github.com/swiftlang/swift/issues/81274 is fixed.
    public mutating func withValue<R: ~Copyable, E: Error>(
        _ work: (inout sending Value) throws(E) -> sending R
    ) throws(E) -> sending R {
        try work(&self.value)
    }
}

As you can see, the compiler is not ready for this :sweat_smile:

2 Likes

Several years ago during early days of Swift concurrency we had to rely on workarounds like MainActor.assumeIsolated , Task.detached, @preconcurrency import and other workarounds. Today concurrency has improved significantly and we need such workarounds far less rarely, so I don't worry about future RBI evolution.

At the same time Disconnected feels like an ad‑hoc solution for current compiler limitations. We need a clear vision: which cases will still require Disconnected even after future RBI improvements, and which one will be solved by those improvements.

That said, I think a written roadmap for RBI evolution would be very helpful.

Disconnected is understandable for those of us already deeply familiar with Swift’s concurrency model. But for the vast majority of Swift users this term is quite strange – it looks like network disconnection or broken component state.

A name like Sending<T> feels more discoverable and clearer, especially in the context of the existing sending parameter annotation and Sendable protocol. It directly expresses the action (sending across regions) rather than the state (being disconnected).

Additional points to consider:

  • Disconnected does not compose as naturally with sending and Sendable as Sending<T> would.
  • Sending<T> implicitly references term “region” without introducing a new adjective.

It seems nowadays Disconnected types are primarily created by library implementers. But as Swift 6 mode sees wider adoption this primitive will inevitably be used by a much larger and more diverse audience. For that broader group, Disconnected feels like an ill‑fitting name.

7 Likes

I believe defining take as consuming instead of mutating will limit the usability of this type. For example, the code snippet from the "Proposed solution` section will not compile:

guard let disconnected = deque.popFirst() else { return }

Task {
    let element = disconnected.take()   //  <<-- This line should contain a compiler error
    print(element)
}

disconnected cannot be consumed in the task closure under the current rules, it can only be used in a borrowing way.

1 Like

From my testing on the latest nightly snapshot, all of those problems have been resolved. I am curious about the SendableMetatype constraint you had to add. I think this might be required due to the ExpressibleByNilLiteral constraint, which the pitched API here doesn't require.

Implementation

This is the current implementation that I am using:

struct Disconnected<Value: ~Copyable>: ~Copyable, Sendable {
    // This is safe since we take the value as sending and take consumes it
    // and returns it as sending.
    private nonisolated(unsafe) var _value: Value

    @usableFromInline
    init(value: consuming sending Value) {
        self._value = value
    }

    @usableFromInline
    consuming func take() -> sending Value {
        nonisolated(unsafe) let value = consume self._value
        return value
    }

    @usableFromInline
    mutating func swap(newValue: consuming sending Value) -> sending Value {
        nonisolated(unsafe) let value = consume self._value
        self = Disconnected(value: newValue)
        return value
    }
}

You are right that example was slightly off and should have been like this:

var deque = UniqueDeque<Disconnected<NonSendable>>()
deque.append(Disconnected(NonSendable()))

guard var disconnected = deque.popFirst() else { return }
let element = disconnected.take()

Task {
    print(element)
}

This is due to the limitation I mentioned above that Swift doesn't have call-once closures at this point. Otherwise, the example would have worked. I updated the example in the PR.

The need for Disconnected will stay even with improvements to RBI in my opinion. I tried to motivate this in the evolution proposal by showing the concrete example of storing and retrieving a disconnected value in a container. This is the primary motivation behind this type and will remain even if Swift gains call-once closures and more improvements to RBI. This type is similar to the recently accepted Ref and MutableRef types in the sense that these types allow us to encode the ownership of a value in the type system, where Disconnected allows us to encode the disconnectedness of a value in the type system.

I am not opposed to adding this. The reason that I haven't done it yet is that I have not come across a use-case where this extension was required. That's mainly because the places where I see Disconnected being used are in generic contexts where the Value is not known to be Sendable. Those generic contexts take a sending Value and return a sending Value. Disconnected is merely an implementation detail that allows upholding the disconnectedness. I don't expect many if not any API to accept/return a Disconnected value. APIs should either accept/return a sending Value or accept/return a generic Value: ~Copyable which can be a Disconnected<Value>.

2 Likes

That's good news.

Our names don't match up — your take is my consume, where my take is a special case of swap by analogy to Optional.take (and anticipating that Disconnected<T?> will be a common usage; certainly it has been in my experience)

I just came across a need for having a withValue method myself when I wanted to briefly compute something on the disconnected value. Added the following method to the pitch similar to what @KeithBauerANZ had in his type:

mutating func withValue<Return: ~Copyable, Failure>(
    body: (inout sending Value) throws(Failure) -> Return
) throws(Failure) -> Return

As for this concrete example:

I strongly feel that this should be modelled in a different way rather than wrapping elements in a Disconnected wrapper. For now, given current compiler limitations, it may be an acceptable workaround. But Swift concurrency is a general‑purpose feature used by the vast majority of mobile and desktop developers. Forcing them to manually wrap values in a Disconnected container feels like a poor language design:

  1. Boilerplate and friction – every use of a generic container with disconnected values now requires explicit wrapping and unwrapping (Disconnected(x) , then .take() ). It is ergonomic issue.
  2. Discoverability – new users will not intuitively know that Disconnected is the solution to passing non‑Sendable values through a container. They will first try sending annotations, fail, and then search for workarounds.
  3. Composition overhead – wrapping a value changes its type, which complicates generic code that expects to work with both disconnected and non‑disconnected elements.

There might also be performance costs.

I understand that compiler can not currently propagate sending through stored properties. But instead of pushing the burden onto every library maintainer and users I'd prefer to see a language solution, perhaps a way to mark a container's methods as conditionally sending based on the element type and a sending generic parameter. The Disconnected wrapper is a useful stopgap but it should not become the final design.

That are the reasons I wrote we need a broader vision on which kind of problems will solved by RBI improvements and which will be not.

To be clear I'm not saying that this should be described within this proposal. In any case, we already have to write our own Disconnected wrappers and it's better to have one built into the stdlib.

5 Likes

+1 Exactly my thoughts after thinking about it some more.

Deleted

Would it work to change popFirst to return a sending value? I doubt it because RBI can only merge regions not split regions. An example:

class NonSendable {}

struct MyContainer {
    var item: NonSendable?

    mutating func getAndSend() -> sending NonSendable? {
        guard let item else { return nil }
        self.item = nil
        return item // error: sending 'item.some' risks causing data races
    }
}

The proposed Disconnected wrapper works because it's Sendable.

EDIT: I saw you mentioned requirements like consuming the element in append, but I don’t think that alone is sufficient. Containers in pratical code have much larger API surfaces than Disconnected, making them hard to get right. I actually think it’s impossible to get them working without Disconnected, because there are no way to prove to compiler that different elements in a container don't reference to each other (please correct me if I’m wrong). That’s why I don’t agree with @Dmitriy_Ignatyev. Disconnected is the only approach that works in some scenarios. On the other hand it’s unknown how long we would need to wait for a similar language feature. Regarding concerns about boilerplate code, it can be hided from user. See an example in this post.

Sorry, I just realized that it's always possible to use nonisolated(unsafe) if one is sure it doesn't cause problem. IMO using Disconnected wrapper is much safer than using nonisolated(unsafe) in ad-hoc way. +1 from me.

I feel that the name Disconnected is too, uh, disconnected from its usage among the current concurrency modifiers, but I leave it to others to come up with suggestions for a better name. If this is all about what's Sendable then surely the name should involve "Send" in some way, to help the ignorant user (meaning me).