Swift Vision: Improving the Approachability of Data-Race Safety
Holly Borla (via Mastodon, forum):
This document lays out several potential paths for improving the usability of Swift 6, especially in simple situations where users aren’t intending to use concurrency at all.
[…]
A key tenet of our thinking in this vision is that we want to drastically reduce the number of explicit concurrency annotations necessary in projects that aren’t trying to leverage parallelism for performance. This is important for many kinds of programming, such as UI programming and scripts, where concurrency is often localized and large swathes of the code are generally expected to be constrained to the main actor. At the same time, we want to maintain a smooth path for experienced programmers to opt in to concurrency and maintain the safety of complete data-race checking.
[…]
We believe that the right solution to these problems is to allow code to opt in to being “single-threaded” by default, on a module-by-module basis. This would change the default isolation rule for unannotated code in the module: rather than being non-isolated, and therefore having to deal with the presumption of concurrency, the code would instead be implicitly isolated to
@MainActor
. Code imported from other modules would be unaffected by the current module’s choice of default.[…]
Adding a per-module setting to specify the default isolation would introduce a new permanent language dialect. […] On balance, we feel that the costs of this particular dialect are modest and manageable.
[…]
The most important of these for our model of single-threaded code is to be able to express global-actor-isolated conformances. When a type is isolated to a global actor, its methods will be isolated by default. Normally, these methods would not be legal implementations of nonisolated protocol requirements. When Swift recognizes this, it can simply treat the conformance as isolated to that global actor. This is a kind of isolated conformance, which will be a new concept in the language.
what I’ve been complaining about since when first Actors introduced to Swift is that it forces “async-first” instead of “sync-first” programming. I’m super happy the Swift Language Steering Group has finally noticed it.
So much effort poured into this, and then the DX problem can be summarized like “you can’t easily write single-threaded code now anymore” 🤯
That said, for such a broad vision, I’m missing coverage of the present issues with isolation behavior mismatches to Objective-C code and Objective-C system libraries.
To this date, much of Apple’s new platform development is still done in ObjC, and many – if not most – Apple platform developers can’t evade UIKit or AppKit in their day-to-day work. Yet this language does not know anything about strict concurrency and allows comfortably programming in very non-compatible ways. What I dearly miss are tools to bridge this gap, to make using those APIs from Swift 6 as comfortable as it is from Swift 5 or ObjC.
[…]
Most importantly, to me, we need more robust and flexible ways to declare dynamic
MainActor
isolation. BasicallyMainActor.assumeIsolated
but for entire classes and without all the sendability-dance for passing things in and out of that closure. (#isolation
being non-nil when called from the ObjC main thread would also be nice.)[…]
I struggle to write this more precisely in abstract terms, so I’d like to give an example from my recent work with TextKit2 – a fairly new system iOS/macOS API, that’s thought and written entirely in ObjC.
My understanding is that the document says that one of the reasons that analyzing the program as a whole is bad is because “it would make the first adoption of concurrency extremely painful”. Then, it goes on to say that a better approach is to make the single-threaded assumption in smaller parts of the program. Finally, the document proposes that these smaller parts are the modules.
Choosing the modules as the smaller parts have caught my attention because over the last 3 years I’ve interacted with a couple dozens of beginner Swift programmers and the vast majority of the apps I’ve seen them develop do not have the code they’ve written broken down in smaller modules. The apps are mostly composed by 1 module + dependencies.
Is breaking out of the single-threaded default on these projects with one big module possibly going to be quite painful?
Previously:
- Swift Concurrency Proposal Index
- Problematic Swift Concurrency Patterns
- Swift Concurrency and Objective-C
- Unwanted Swift Concurrency Checking
- Swift 6
Update (2024-11-26): Rob Jonson:
I think you’re absolutely right to focus on these - but I would argue for a radically different approach.
[…]
Flip the default. The default should be that guaranteed data race safety is turned off. […] Moving to an opt-in model will change the dynamic. At the moment, it feels like we’re on a forced march to the promised land of Swift 6 safety. If safety is opt in, then developers will choose to use it as it becomes more ergonomic. If the feature has to be worth the pain to convince people to opt in, - the dynamic around design will focus more on real usage.
[…]
Analyse code was a great tool as we moved towards arc (and even later). Run the tool, examine warnings about memory safety, fix if needed. […] Concurrency could do the same thing. Analyse could warn me that returning an NSImage is potentially unsafe if the sender keeps and mutates the original - but I can choose to ignore that because I know I’m not doing so.
I’m not sure I agree with this, but it’s interesting to consider.
Tim:
I remember the exact same arguments about optionals when Swift was first released. “I know what I’m doing”. “The compiler is trying to baby me” etc etc. Understanding optionals is definitely far easier than concurrency, but it’s the same thing of a language feature tackling common programming errors and I think we can all agree that it’s been a great feature once understood.
I’ve always been in favor of optionals, but I think the other way of looking at this is that people quickly saw that optionals provided real benefits in reliability and code clarity at very little cost (cognitive or visual). Beyond the async/await
sugar, Swift Concurrency’s costs seem much higher and its benefits less clear.
Update (2024-12-02): See also: Hacker News.
I’m not on the Swift forums much, but occasionally I follow the links in the summary emails.
It is very interesting how much developer sentiment towards the language has shifted over the past few years.
I’m not here to be a hater, but I certainly feel the frustration.
The thread in question:
Swift is such a mess. How has it got to a point where returning an object from a function requires two undocumented language features?
And, believe it or not, this vision doc changes would fix most (but not all) of the technical problems this particular person has run into.
I think Alex echoes a wide sentiment: that people want to write software and not be a slave of the latest features. You say to not use Swift 6. Well, guess what, my project depends on Vapor and unless I’m OK being stuck with an older version (I’m not), I’m forced to use Swift 6. So there you have it. Structured Concurrency down our throats.
I feel like even with all of this, some of the most painful points are being missed.
For example, there’s the idea that programmers should start single threaded and slowly move to concurrency - but you can’t actually do that in real life, IMO. Libraries/frameworks constantly impose concurrency on you - either they have async functions or they’re using callbacks where stuff happens “later” and often on a different thread/actor or the function is marked nonisolated (for good reason), etc.
You can’t ease into it.