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.
6 Comments RSS · Twitter · Mastodon
Instead of evolving Swift 6, why not approach Swift 7 as a chance to have another attempt at evolving from Swift 5? We can consider Swift 6 a write-off. It would take courage to admit that Swift 6 was a misstep but in the long run it would be a much more effective way to put the many lessons learned into practice. Any Swift 6 codebase under development (Iād be surprised if there are many) could currently easily revert back to Swift 5.
This looks like a lot of busywork trash that you will end up regretting after you introduce it in your codebase
@ObjC4Life - I can't agree more!
Swift has never jived with me, and I've tried and tried since it was released. The ergonomics are just awful, and stuff like makes me feel like I can't make any serious investment in that environment.
I recently created a new Xcode project, and instead of picking Swift, I'm architecting this project like AppKit + Objective-C and EMBEDDING a "self-contained" compiled C# .NET binary inside of it for all of the logic/services! I really love where MS has taken .NET, it's so nice and fast, and has the modern language features. Obj-C is just easier/less impedance to work with wrt AppKit.
Heading into 2025, I'm locked in on: Objective-C and C#. For anything that's scripting-related, I'm over the cognitive burden of scripting languages, replacing that with hyperscript (and AppleScript if running on MacOS). Will be continuing with HTMX for web application front-ends.
Looking at what Apple has done with their development technologies since .NET open sourced is... it's really sad comparison. They could have revived the Cocoa-Java bridge and probably been in a better position by now.
I'll say again. Swift is the Homer Simpson car of programming languages. Features are just piled on and on and on, with no pause given to think about what the rammifications are.
@benedict Like PHP 6 --> PHP 7.
Where was the realization and pushback in the proposal phase? This feels like teams at Apple finally got ordered to adopt Swift 6, used it for 2 minutes, and said "NOPE!" The Swift Evolution Hugbox strikes again.
--
I want to read more about what devs learned or fixed by adopting Swift 6. Are there new insights or cautionary tales about particular design patterns or APIs? It feels like most 'successes' are proclamations of "I got it working! Now I'm future-proofed!" instead of discussing what was actually learned or specific problems fixed.
This huge upheaval and complication of existing and interoperable Swift codebases seems to based upon the premise that data-race safety is a huge issue that needs to be solved and eradicated everywhere irrespective of the cost of doing so.
All engineering is a trade off. Domain experts who use those tools ā e.g. app developers ā are best placed to judge if the effort required to guarantee that a problem won't occur is worth it in their situation. Swift 6 concurrency is designed on the premise that everyone should pay that cost.
Swift is mostly used for creating iOS apps on the App Store. How many of these are crashing or expending hours of developers time due to a multitude of data-race safety issues? The response will be: "but Swift is meant to be used in more than just apps", when in reality that doesn't seem to be happening much.
The real issue is that this mess highlights how far the Swift language designers seem to be from the majority of the users of their language.
No-one asked for Swift 6 concurrency and yet the Swift language designers seem to think that constantly adding more features and syntax to Swift, with ever-increasing complexity, is the best way forward.