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!

Swift Evolution Monthly: December '23

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
‎Welcome to CrossCraft, where your imagination meets the classic charm of crossword puzzles! This innovative app transforms the traditional crossword experience, offering an unparalleled level of customization and personalization. ----- Varied Topics ----- Dive into an extensive range of topics from…

Creating, playing, and sharing crossword puzzles is free!

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:

  1. When no file exists at the given path
  2. When the app has no access to the path (Sandbox)
  3. 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
   }
}

The error in the catch block is of type CSVParsingError, not any Error!

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:

extension UITableView {
   protocol Delegate {
      // ...
   }
}

To reference the protocol, you would write UITableView.Delegate.

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. 🤞

✉️
Don't want to miss future issues? Subscribe to the newsletter for free.

Other Accepted Proposals

Proposals in Progress

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.