I realize it's probably too late to make any difference in Swift's evolution, but I get the sense this whole area, and especially its consequences for language complexity, hasn't been well thought out.
99.9% of uses of ownership in Rust can be expressed in Swift by yielding subscripts (or properties) with utter simplicity. I wager the failure to recognize that fact and instead return Spans by value accounts for all these deprecations. As far as I can tell, the only thing you get from ~Escapable that you don't get from yield, is the ability to assign the result into a variable, which would be covered if Swift added the missing ability to create named inout and borrowing bindings.
I may have missed a lot—there are surely details of the design I don't yet understand well—but my overall concern stands: I've thought a lot about the kinds of use cases that a complex borrow checking system like the one that seems to be evolving here enables and I'm convinced that the vast majority of real use-cases can be covered by subscripts and two new types of bindings, and that a far, far easier and simpler overall language results from being willing to use unsafe constructs for the small corner of code that remains.
That's the bet Hylo is making. We even have a (partially-designed) feature called “remote parts” that would help us cover most or all of that small corner without introducing the kind of complexity I see developing here, but we are being circumspect because we're not sure how often it's actually needed and thus whether it will carry its weight as a language feature. I fear that Swift has lost the hard-nosed unwillingness that we started with to solve problems by becoming more complex. I care about this because Swift is still the language I choose for real programming, mostly due to its ability to express my intent directly and efficiently without noise getting in my way. On my value scale it's simply the best general-purpose language, and I'd like to keep it that way.
From a retrospective view it might give the impression that some aspects weren’t fully explored, but at each stage the design has been carefully considered, reviewed from multiple angles and adjusted or sent back for revision.
Now when a significant part of the path has already been walked, we can look back and draw some conclusions. But I believe that at each point the best decisions available were made with the information and constraints at hand.
I’m not deeply experienced with Rust, but from what I understand, ownership usage in Rust is often temporary, non-escaping and used locally. In that sense it maps very closely to Swift’s yielding accessors, as you noted.
Yielding subscripts cover only a subset of Rust’s ownership capabilities, not anywhere near 99.9%. There also borrow bindings, stored references, borrowing iterations, repeated access without recomputing the projection, generics. Yielding accessors alone cannot express all of these patterns. However, yielding accessors combined with the recently pitched Borrow and Inout types seem capable of expressing a large fraction of commonly written Rust borrowing code.
As I feel it, Swift’s ~Escapable is about choosing a different point in the complexity-ergonomics tradeoff space. I find Swift ergonomics noticeably higher than in Rust. It keeps the common cases simple and lightweight, at the cost of some expressivity.
It gives developers non-escaping guarantees without the cognitive and syntactic burden of explicit lifetime annotations.
One more great benefit I see is that lifetimes in Swift are not required in API most of the time. The compiler ensures safety without exposing lifetimes in the func signatures.
Most of this complexity is hidden. When I talk with mobile developers, most of them don’t really know what ownership is, and those who have ever seen consuming somewhere usually think something like: “this is for people who understand it and need it, not for me.”
For those who do need it, the complexity comes from the real world no matter what language you use: Swift, Rust, or C++. If you want to do things correctly, precisely and safely, you have to deal with it. In C++, you don’t get as many compile-time guarantees and must rely on your own understanding to enforce safety.
In Swift and Rust, the compiler is the guarantor of safety;
in C++, you are the guarantor.
In any case, the complexity arises from the way memory and low-level computer operations actually work. Safety and correctness are not abstract concepts; they are reflections of real world constraints.
Swift’s ownership model is rather pragmatic – it captures most real-world needs for safe, temporary or localized references while keeping the APIs clean and ergonomically friendly. It allows developers to express intent safely without being constantly burdened by low-level details, unlike C++ or even Rust in more complex lifetime scenarios.
This balance between safety and usability is what makes it approachable for most developers while still being precise enough.
So, going back to the phrase:
Personally, I don’t feel that way. To me Swift’s ownership model is moving in the right direction and aligns very well with the principle of progressive disclosure.
I'm certain that the design has been carefully considered at each stage. I'm saying that in doing this incrementally by adding “small” features—each of which has arguably only a small complexity cost—the massive opportunity provided by existing features has been overlooked. (This is not the first such failure, IMO).
Yes, in fact that is an inherent part of the “shape” of computations implied by the Law of Exclusivity. It is this shape that I believe has not been carefully considered. References in Rust (and borrows/ inouts/~Escaping in Swift, or similar constructs in any language that statically upholds the LoE) are not generalized references at all, because of these limitations:
The LoE is upheld by the compiler (a good thing!) which means a lot of code that would respect the LoE if analyzed dynamically has to be expressed differently.
You may be able to store them temporarily (because of the previous bullet) but you can never store (or even get) multiple mutable references into the same whole object without some unsafe code that exposes multiple non-overlapping parts of a single whole at once. That severely limits what you can express with them, and it's not clear what compelling use-cases remain that justify the complexity.
Yielding subscripts cover only a subset of Rust’s ownership capabilities, not anywhere near 99.9%.
I didn't make any claim about a percentage of Rust's ownership capabilities. It is a claim about use-cases. In fact, you're proving my point: most of these other capabilities account for a lot of complexity and language weight and are only “needed” in a small fraction of use cases.
There also borrow bindings
Which I mentioned as a missing piece for Swift (which IMO should have been added before any of this other stuff because synergy with yielding accessors would have helped reveal that more complexity isn't needed), but which can also be expressed as parameters to higher-order functions.
stored references
As noted, it's really unclear how important or useful this feature is. It is the same capability as the aforementioned “remote parts” which Hylo is currently refraining from surfacing because we're not sure it's a capability/complexity win, but notably we believe would still be much less complex than first-class references.
borrowing iterations
Easily expressed using yielding accessors.
repeated access without recomputing the projection
That's just borrow bindings again, is it not?
generics
I don't know what you have in mind here, but the way lifetime annotations creep into the type system via generics is among the worst complexity effects they've had on Rust.
yielding accessors combined with the recently pitched Borrow and Inout types seem capable of expressing a large fraction of commonly written Rust borrowing code.
My point exactly ! Swift could avoid a huge amount of complexity by starting there. (Though the idea that there would be first class types rather than binding modifiers is extremely worrying. Where is this pitch?)
As for complexity being hidden:
It is a fallacy that complexity in the fundamental language base is ever really hidden. It's always there for anyone who wants to really understand what's going on. That is even true for features such as Builders that sit on top, but at least there's a level of the language you can fully understand without confronting them.
This is an effect of the incremental adoption and rollout. It only seems hidden because the standard library hasn't (fully?) adopted it, at which point it is going to be in everyone's faces. It will also become an imperative for library developers to understand and support these features.
For those who do need it, the complexity comes from the real world no matter what language you use: Swift, Rust, or C++.
That's an oversimplification. There are tradeoffs here. The vast majority of these problems are easily solved by a safe language with a few well-chosen features. There's a small corner of problems that Rust can solve safely, but only at a huge cost in language complexity, and in complexity of the code that solves those problems. It is much harder to write and verify the correctness of that code than of the same code written in an unsafe language, because of all the hoops you have to jump through. Not everything is best expressed in the type system, and the cases that aren't expressible with yielding accessors and two new bindings are so rare as to not be worth supporting in safe code.
It's not entirely true that the compiler is the guarantor of safety. The standard library plays a huge role. There are some things that can't be implemented in safe code (like Array) so the standard library implements them as safe abstractions. Because the unsafe code is localized, validating the correctness of these abstractions isn't hard. The same facilities are available to end users for good reason. Did anybody research the real use cases in this corner and consider solving them that way vs the complexity costs of these new features?
I bet they didn't, but if I'm wrong, someone should be able to provide examples that fit the shape described above and become much better and cleaner with all these new language features than they would be without them. The fact that we in Hylo land have been having this same conversation with Rust folks and haven't gotten a single concrete example makes us strongly suspect the same is true for Swift. I wouldn't have posted this otherwise.
There is not much I can add to Dave’s response but I’d like to say that we’re genuinely interested in discovering those use cases that are truly not expressible with yielding accessors and borrow bindings, but still fit within the shape that Dave mentioned. We do not deny that those cases exist but we find them very rare in practice, which is part of why we’re betting that Hylo does not need Rust-like references to express the kind of programs people write in safe Rust or Swift.
I would encourage people interested in trying to understand what we mean to try applying this pattern every time they feel like they need a function returning a reference:
This transformation is sufficient to avoid the kind of escaping references that Rust offers in the vast majority of cases I have encountered in practice. Then, one can observe that references that only appear as non-escaping arguments can be expressed with either by-value or inout parameters in Swift.
yielding accessors and borrow bindings do not add much expressiveness to this pattern; they can be understood as a sugar to make it easier to use.
Yielding subscripts cover only a subset of Rust’s ownership capabilities, not anywhere near 99.9%.
As Dave mentioned, I don’t think that is true as soon as borrowing bindings are considered. I’ll add two things:
First, a borrowing binding can be expressed as a parameter of some function. So, again as an exercise, one can combine yielding accessors with functions to get a sense of what’s possible to express without Rust-like references. The following illustrates with an actual pattern that we use in many places of Hylo’s compiler implementation:
public func modify<T, U>(_ value: inout T, _ action: (inout T) throws -> U) rethrows -> U {
try action(&value)
}
public struct DirectedGraph<Vertex: Hashable, Label> {
private var out: [Vertex: [Vertex: Label]]
public mutating func removeEdge(from source: Vertex, to target: Vertex) -> Label? {
modify(&out[source, default: [:]]) { (tips) in
if let i = tips.index(forKey: target) {
defer { tips.remove(at: i) }
return tips[i].value
} else {
return nil
}
}
}
}
In this example, we use modify in combination with Dictionary's subscript to essentially reproduce Rust’s HashMap::entry method. Borrowing bindings would simplify the example by eliminating the higher-order function but, crucially, Swift is already expressive enough to use a pattern that many in the Rust community present as a justification for first-class references.
Second, it’s important to observe that subscripts allow patterns that references do not. Therefore yielding accessors do not encode just a strict subset of references. There are plenty examples in Swift’s standard library already but an academic one that I like is the following:
extension UInt32: MutableCollection {
public var startIndex: Int { 0 }
public var endIndex: Int { 4 }
public func index(after p: Int) -> Int { p + 1 }
public subscript(p: Int) -> UInt8 {
get { UInt8(truncatingIfNeeded: self >> (p * 8)) }
_modify {
var u = UInt8(truncatingIfNeeded: self >> (p * 8))
yield &u
self = self & ~(0xff << (p * 8)) | (UInt32(u) << (p * 8))
}
}
}
func foo<T: FixedWidthInteger>(_ x: inout T, _ y: T) { x = ~x & y }
var xs: UInt32 = 0x01020304
foo(&xs[2], 0xff)
print(String(xs, radix: 16)) // 0x01fd0304
AFAICT it is not possible to write an equivalent to the _modify variant of the above subscript without using some kind of proxy, which would invite more complexity. In particular, it would not let us apply foo without paying for a more sophisticated signature. I believe that explains in part why conversions are so pervasive in Rust.
Wasn't one of the explicit motivations for spans to help eliminate the need for these higher order function wrappers? Particularly, to obviate the use of the various withUnsafeXXX functions? Unless the unsafeness of the types vended by those methods was the only (or nearly the only) reason the core team wanted to wean us off of them, then I don't see how proving that first class references are equivalent to the use of withXXX closures changes anything.
Aside, perhaps, from proving the soundness of first class references.
Sure, but what are the actual downsides? Developers can still write Swift in a Swift 3 style today, and many do. I regularly see code that only uses Swift 3/4–level features, even though Swift 5/6 has been available for years and offers significantly better language features and tooling.
Of course, all of these features add complexity. However, we’re not forced to use them. We’re not required to define ~Copyable types, and we can still build libraries without ~Escapable types. If you don’t want to engage with these features, you can design libraries using only yielding accessors with Borrow and Inout. And you can still use unsafe constructs if you prefer them instead of these new features.
Am I missing something in making this argument?
Our discussion is happening in the space of meanings and features at the leading edge of Swift’s evolution, including experimental, upcoming, and even not-yet-implemented features. However, my experience shows that a large number of developers are only fragmentarily and selectively aware of new Swift capabilities. Many developers still don’t know about Never, Duration, variadic generics, or typed throws, and even fewer are aware of move-only types, init accessors, InlineArray, or region-based isolation.
Another example is opaque types. A huge number of developers have already encountered them through SwiftUI, yet many of those developers don’t use opaque types anywhere outside SwiftUI, largely because they have a weak understanding of what they are for beyond that context.
So yes, the language does become more complex. But what is the actual problem, and for whom? Most developers will never have to face this complexity at all.
Those who do need advanced features now simply have more options: they can use the new language features, or continue to rely on unsafe primitives, as is commonly done today for e.g. Array.
PS: Despite everything said above, and even though I have my own perspective on this situation, I’m genuinely interested in understanding how you see it. I’d like to hear your viewpoint and I find your perspective interesting.
The higher-order function wrapper only serves to compensate for the lack of borrowing binding. If it was possible to create a local inout binding there would be no need for the modify function in the example I presented.
In Hylo one can write the following:
subscript min<T is Comparable>(x: inout T, y: inout T) inout -> T {
if y < x { yield &y } else { yield &x }
}
public fun main() {
var x = 1
var y = (2, 0)
inout z = &min[&x, &y.1]
&z += 1
}
Not using a feature does not prevent the complexity of the feature from leaking in other aspects of the language.
Dave mentioned libraries. As a library author, whether I like it or not, I will eventually have to contend with ~Escapable and think about the interaction of this feature with my algorithms and data structures. Maybe I can make the choice to ignore non-escapable types, but even that requires recognizing their existence and understanding them to make an informed choice.
Another issue relates to error messages. Whether or not one knows about protocols, one will inevitably encounter an error message that talks about them when one uses Swift. Understanding the diagnostic presented by the compiler, therefore, requires at least some understanding of the feature. This phenomenon is quite apparent in Rust in my opinion, as it is effectively impossible to simply ignore lifetime annotations even if one decides to never use references in their own code.
I am more interested in concrete code examples and real use-cases that has been motivation for this path and can’t be expressed using yield accessors with Borrow and Inout. Because the language complexity is real, although it is “arguably hidden”, but there should be many motivating examples for adding even that hidden complexity, right?
As someone who mostly lurks on this forum, I am aware of these features, but as a typical app developer, I don’t use most of them. My experience matches your intuition that most developers will never need to master these concepts and features in order to continue using Swift.
Usage
When I write that I don’t use them, I mean that I don’t write code that explicitly makes use of the feature. I don’t mean that I don’t use stdlib or third-party code that uses these features.
I’m not sure I fully understand your position. Are you suggesting that the language should stop evolving? Are you opposed to specific features, or do you see some of them as harmful? Do you have alternative approaches in mind that could provide game-changing benefits? Ideally, what do you want to see instead?
Swift has become a mature language, and every mature language accumulates this kind of complexity. C is much simpler in terms of surface features, but it’s hard to imagine most developers being taught to write safe programs in C. By contrast, many developers today write relatively safe Swift code because safety checks and best practices are built into the compiler. As a result people can write reasonably safe code by default without having deep expertise.
We are getting off topic but I am happy to clarify my position.
I have no problem for any language evolving. However, all features come with complexity costs that must be considered with respect to the benefits they bring. I would consider adding a new feature to a well-established language if and only if the result of this cost/benefit analysis leans heavily toward the latter. I firmly believe that complexity causes harm and friction in terms of explainability, user experience, and maintenance, regardless of how well/often it can be hidden under the rug with good defaults and/or inference. Hiding tricks almost always leak and as a teacher and researcher, I can tell from experience that it makes languages harder to explain and/or study.
Specifically in the context of this thread, we are talking about multiple features adding significant complexity to the language and I am not convinced that the benefits they bring are worth this complexity, especially because I believe that the overwhelming majority of use cases justifying these features can be addressed with much simpler changes (i.e., borrowing bindings). In other words, I do not believe these features (~Escapable, named lifetimes, first-class references, etc.) are game changer in Swift because yielding accessors have already changed the game.
How could Span types be implemented safely without ~Escapable? Are there alternatives that give the same strong safety guarantees? Without it, returning non-owning references to parts of a collection is either unsafe or forces copying, losing efficiency and clarity.
Take collection slices for example. Today, all slices carry a warning:
“Long-term storage of a slice may prolong the lifetime of elements that are no longer otherwise accessible, which can appear as memory leakage.”
With ~Escapable, we can create slices that cannot outlive their storage, removing this hazard. Slices aren’t a marginal or narrow feature, they’re broadly used. The same applies to other temporary views: substrings, windowed computations, cursors... anything that should not escape its underlying data.
Swift already distinguishes between escaping and non-escaping closures, with non-escaping closures being extremely common. ~Escapable extends this concept to other types, allowing safe, temporary borrows in a broader range of situations.
As for Ownership Annotations, they are useful for Copyable types too. As Swift team members already pointed out, even perfect optimizer will not be able to eliminate all copies. If you have ideas how to completely eliminate copies overhead, which is needed in performance constrained code, I'm sure those ideas will be highly welcome in Swift community. Bot for now it is the best we have.
I think I was confused by the thread’s title. Is the main argument here that we don’t need ~Escapable? That exposing a ~Copyable view type through a borrowing/mutating variable/subscript is sufficient to guarantee that said view doesn’t escape?
Developers may be unaware of these features because Swift does a poor job of surfacing their existence at the level most developers operate, and makes it difficult to try them out. I've never been able to use a toolchain to build an iOS app, which is still the vast majority of Swift code.
Never: I'd actually say this is fairly common for anyone writing or using generic code. You have to at least type the word to use Combine, for example.
Duration: Types with limited OS availability will naturally see limited use. iOS 16 only just became a common deployment target in the last year for many companies.
variadic generics: Useful feature that is extremely buggy (or incomplete), so is avoided or dropped. It shouldn't be a surprise that, when a feature turns up bug reports or other issues in search results, it doesn't become popular.
typed throws: Incomplete feature missing the most useful part, inference through closures. Unless you know the additional syntax required to let it be even partly useful, it just looks broken.
move-only types: Niche feature that doesn't have any obvious use cases for most developers. I've only ever used it because that was the only way to get a logger working with @_transparent to call the runtime issue API.
init accessors: Also niche, with odd syntax, so it isn't discoverable, but I have seen it used where appropriate in knowledgeable libraries.
InlineArray: Also niche, and also runtime limited, so I wouldn't expect to see much usage until later this year at the earliest, probably next year.
region-based isolation: This isn't a developer feature, and is enabled by default in Swift 6 mode, so all Swift 6 devs use it. I wouldn't expect developers to know about it at all, any more than other compiler intelligence features, but anyone who's transitioned from Swift 5 to 6 definitely knows about it, since it's a huge part of the compiler's concurrency intelligence that makes the feature actually work.
If Swift would like to make its features more discoverable, that's great. A linter for adopting new language features to better replace existing patterns would be well received. But given Swift's limited attempts to encourage adoption, and deployment limitations on its most popular platform, you can't really use such limited adoption to say that evolution at the cutting edge can proceed without considering such adoption.
Personally, I would like to see an evolution freeze for at least a year, focusing on bug fixes, documentation improvements, and an evaluation of Swift’s existing and envisioned features, perhaps even reconsidering entire features.
P.S. For example, Swift’s initial actor implementation was completely broken and clearly not ready at the time.
To be clear (answering @filip-sakel and clarifying for @Dmitriy_Ignatyev), ~Escapable is not the primary problem. The primary problem is the steady march toward a full Rust-like lifetime system, which is where you end up as soon as you start returning (instead of yielding) notional references from one thing into another. We believe escapability shouldn't be surfaced as a first-class property of types. All that said, ~Escapable seems to be entirely unneeded today.
I was surprised to find that it seems to be possible today without any explicit treatment of escapability.
With ~Escapable, we can create slices that cannot outlive their storage, removing this hazard. Slices aren’t a marginal or narrow feature, they’re broadly used. The same applies to other temporary views: substrings, windowed computations, cursors... anything that should not escape its underlying data.
All of which you can do by yielding ~Copyable things.â€
Swift already distinguishes between escaping and non-escaping closures, with non-escaping closures being extremely common. ~Escapable extends this concept to other types, allowing safe, temporary borrows in a broader range of situations.
None of this is lost on me, you know. I saw this complexity coming years ago when I pleaded for an effort to address fundamentals first and use these existing concepts to create a more coherent solution.
It's mostly number 2 ;-). As I mentioned, some notion of escapability is needed. We think it shouldn't be surfaced in this way (and arguably I've shown that it doesn't need to be—it can just be a consequence of other things), but this one pseudo-protocol, by itself, doesn't create that much of a problem.
Of course, all of these features add complexity. However, we’re not forced to use them.
I'll just point out that the same argument is often given by proponents of C++ when people say it's too complicated. Complexity adds up, hurting usability, teachability, implementability, evolvability, understandability, and adoption (…I could go on…and on!) of a language. These effects are well understood among language designers. When we were first designing Swift, we stayed keenly aware of that fact and pushed back hard against the introduction of new wrinkles. The larger point is that this ethos seems to have been mostly lost. The number of partially-implemented, broken, or limited features in @Jon_Shier's list seem to reflect that.
†When you're thinking about all these generalized uses of non-escapability, keep in mind that what you get from putting it in the type system is limited in the same way Rust references are (though not any more limited than yielded things). The compiler has to reason about what might escape based on only function signatures, so it's easy to come up with useful things you might want to do where nothing actually escapes but the compiler has to stop you. For example, in the Hylo implementation we intentionally store substrings of the whole file string all over the place, and we store them far away from that whole file string. Doing that with ~Escapable types would be impossible. To track the lifetime relationships for a span-like type would entail a Rust-like lifetime system and would require threading lifetime parameters through many diverse data structures.