Swift Evolution Monthly: December '23
Our biggest wish came true: Explaining Typed Throws in Swift. Also: Improved namespacing and reduced dependency creep. And 14 more proposals linked!
Back when there was no Swift on Server, I used to develop my backends with Ruby on Rails. And I remember how the Ruby team tried to time their major releases for Christmas which made it feel a bit like a gift from Santa. Now I feel like the Swift team has managed to do the same this year – their gift is the acceptance of Typed Throws into the Swift language! 🎁 Yes, it's really coming. 😍 🎉
But before I get to the proposals, I want to mention 2 things:
You might have noticed that there have been no issues of this newsletter since July. That's simply because I decided that I would no longer attempt to summarize every single proposal. Instead, I'll focus on those that are most interesting to app developers like myself. My summaries for the more low-level or the advanced server-only topics weren't very useful anyway. But I will still link to them for those interested!
Speaking of app development, you could make me a little gift by checking out my newest Indie app creation – it's an app to craft themed & personalized crossword puzzles. You're not a fan of crosswords? But you might still be interested in a fun challenge about a topic you like, such as Swift/iOS Development, Technology, or one of the other 20 topics in various categories. Test your knowledge or prepare a special gift for your loved ones. Try it now & rate it to support me. Thanks! 🙏👇
CrossCraft: Custom Crosswords 🧩
Accepted Proposal Summaries
SE-0413: Typed throws
Links: 📝 Proposal | 💬 Review | ✅ Acceptance
This is probably the most requested Swift feature in recent years. Many have asked for it, but some rejected it, too. Here's what it's all about:
Imagine you use a function you didn't write yourself (or wrote years ago) that is throwing, such as func parseCSVFile(at url: URL) throw -> [Entry]
. Let's say the function throws in 3 different cases:
When no file exists at the given path
When the app has no access to the path (Sandbox)
When the contents of the file have an invalid format
Unless those 3 cases are documented on the function (which even Apple doesn't do on most system APIs), you can't easily know or handle them separately for a custom-tailored experience. You'd have to read through the full implementation of the function and all throwing functions the function calls inside. A time-consuming and error-prone task. Sometimes you don't even have access to the implementation.
For example, if no file exists at the provided path, you might want to traverse all files in the folder and see if you can find a similarly named file, e.g. using a Levenshtein distance of 2 or lower. If you find a similar one, you could ask the user if they meant that file instead. Depending on your use case, this might be a welcome improvement over just showing a generic error message, which just delegates error handling to the user.
With typed throws in Swift, the author of the parseCSVFile
function will have the option to explicitly specify the possible throwing cases. Typically, an enum
type is created to represent the possible cases like so:
enum CSVParsingError: Error {
case noFileFound(url: URL)
case noAccessToPath(url: URL)
case invalidContentFormat(line: Int?)
}
These types often exist already for many throwing functions. However, the function declaration doesn't contain information about the type yet. Now it can:
func parseCSVFile(at url: URL) throws(CSVParsingError) -> [Entry]
The only option API authors had until now to make the error type explicit was to return Result<[Entry], CSVParsingError>
instead, which will no longer be needed. To handle each error case separately, you can then switch-case over the possible errors in the catch
block like so:
do {
let entries = try parseCSVFile(at: userProvidedURL)
// ...
} catch {
switch error {
case .noFileFound: // try to find similarly named ones
case .noAccessToPath: // show UI to fix Sandbox access
case .invalidContentFormat: // ask user: skip line or cancel
}
}
This is a huge improvement, as you know exactly what can fail and can react to each case differently, as I hinted at in the comments. Not only that, you also don't have a default
case, so if you wanted to handle each error with a special UI, you can. No need for a generic text message UI at all, which you always would have needed with untyped errors, cause there could always be an error you missed or that gets added later on. With typed throws, the compiler ensures you handle all cases and fails at compile-time if you don't, even if new cases got added to the function.
Does that mean we can expect all future Apple system APIs to have a documented error type, exposing all possible error cases? No, unfortunately not. This has technical reasons: Apple ships new versions of their operating systems regularly, changing APIs all the time. Users expect apps to run on newer OS versions without the need for us to resubmit our apps. If Apple added a new error case to the above enum, like providedPathIsAFolder
all apps that use a switch-case
would have undefined behavior if that new error occurred. Therefore only those few functions where the error cases are clear forever by the functions nature can adopt typed throws on the system level. Those are probably rare.
But 3rd-party developers are much more flexible as their frameworks are updated and recompiled by the app developer in Xcode, not by the user via a system update. If they specify the error type explicitly, they simply make the errors part of their public API, therefore changes to the errors would break the API. With proper versioning, this is not a problem – which doesn't mean that all functions should adopt typed throws. Some probably still shouldn't. But they have a choice now. And app developers should be able to use typed throws for their project-internal functions without issues.
To not specify an error type, continue to specify your functions as throws
without a type, in which case the old any Error
behavior remains – a throws
effectively is equal to throws(any Error)
. This also means that the change is fully source-compatible and people can adopt typed throws step by step.
There are a lot more details in the proposal about things like subtyping, its relation with the Result
type or rethrows
, and much more. Read it to learn more!
SE-0404: Nested Protocols in Non-Generic Contexts
Links: 📝 Proposal | 💬 Review | ✅ Acceptance
If you have ever defined a protocol type that is closely related to another type, you might have wanted to define the protocol directly inside the type. This has two advantages: It namespaces it to make the connection clear, and when using it inside the type, its name is much shorter and to the point. But protocols had to be defined globally until now. Not anymore – in the future they can be placed in non-generic contexts like classes, structs, or enums. For example, this allows Apple to move protocols like UITableViewDelegate
into related namespaces as follows:
To keep existing code compatible with the rename, they could define a typealias:
typealias UITableViewDelegate = UITableView.Delegate
And of course, we all can use this, too! The future of Swift is going to be more namespaced, which I think can make code more readable overall. 👍
SE-0409: Access-level modifiers on import declarations
📝 Proposal | 💬 Review | ✅ Acceptance
This one is only important to library authors or those who modularize their apps using SwiftPM. It adds the private
, internal
, and public
keywords to import statements. If you private import
a dependency, it's only usable in private
or fileprivate
declarations in the current file. If you internal import
a dependency, you can additionally use the dependency on internal
signatures. Only if you public import
a dependency, you can use it additionally on public
or open
declarations, which is equal to the current behavior of a simple import
.
The purpose of this proposal is to give package authors the possibility to hide implementation details. Because if there's no public import
of a given dependency B in your whole package, the compiler no longer has to make that dependency B available to users of your package A and can strip it. This will help limit dependency creep and will make our package ecosystem healthier as a whole.
Note that with Swift 5, if you don't specify the access level and use a simple import
, it will effectively be treated as a public import
. But starting with Swift 6 this behavior will change, it will be treated as an internal import
instead and no longer be usable in public
declarations. Migration will be easy though, just add public
where you get a compilation error. Xcode might even provide a fix-it. 🤞
Other Accepted Proposals
SE-0387: Swift SDKs for Cross-Compilation
📝 Proposal | 💬 Reviews: 1st, 2nd | ✅ AcceptanceSE-0405: String Initializers with Encoding Validation
📝 Proposal | 💬 Review | ✅ AcceptanceSE-0407: Member Macro Conformances
📝 Proposal | 💬 Review | ✅ AcceptanceSE-0411: Isolated default value expressions
📝 Proposal | 💬 Review | ✅ AcceptanceSE-0412: Strict concurrency for global variables
📝 Proposal | 💬 Review | ✅ Acceptance
Proposals in Progress
SE-0403: Package Manager Mixed Language Target Support
📝 Proposal | 💬 Review | 🔄 ReturnedSE-0406: Backpressure support for AsyncStream
📝 Proposal | 💬 Review | 🔄 ReturnedSE-0410: Low-Level Atomic Operations
📝 Proposal | 💬 Reviews: 1st, 2ndSE-0414: Region based Isolation
📝 Proposal | 💬 Review | 🔄 ReturnedSE-0415: Function Body Macros
📝 Proposal | 💬 Review | 🔄 ReturnedSE-0416: Subtyping for keypath literals as functions
📝 Proposal | 💬 ReviewSE-0418: Inferring
Sendable
for methods and key path literals
📝 Proposal | 💬 Review
Noteworthy Active Threads
And that's it for December. Don't forget to try my new app CrossCraft. 🧩
And happy holidays, everyone 🎄 ☃️ 🎆
👨💻 Want to Connect?
Follow me on 🐦 Twitter (X), on 🧵 Threads, and 🦣 Mastodon.