Skip to main content

What to Expect from Swift 5.7

· 10 min read
Nikita Lazarev-Zubov

Swift 5.7

Swift 5.6 was released just this past March, but the language evolution is unstoppable. There are plenty of things we already know will be included in the next release, so let’s look at the most interesting developments.

Syntax Improvements

Syntax improvements aren’t necessarily very important, but they will quickly start affecting code style, so we’ll start with these.

if let Shorthand

All Swift developers are familiar with optional binding by means of if and guard statements:

let uuid: String? = "e517e38a-261d-4ca5-85f4-9136ace20683"
if let uuid = uuid {
// The UUID string is not optional here.
}

In Swift 5.7, optional binding will become even more concise:

let uuid: String? = "e517e38a-261d-4ca5-85f4-9136ace20683"
if let uuid {
// The UUID string is not optional here.
}

The same trick is also possible with the guard statement:

guard let uuid else {
// The UUID string is nil.
return
}
// The UUID string is not optional from now on.

This isn’t the most important or anticipated language addition, but it looks spectacular, and I’ll definitely make use of it.

Default Values for Generic Parameters

The compiler will finally accept method declarations like this:

func doSomethingWith<Values: Collection>(
_ values: Values = [1, 2, 3]
) { }

Yes, starting from Swift 5.7, we will be able to use default values with generic parameters.

The default value won’t limit the use of the generic parameter, and we’ll still be able to pass, for example, a set of strings if we’re not happy with the default argument.

Multi-Statement Closure Type Inference

The current version of Swift is good enough in inferring types of closures… as long as the closure has only one statement. This code is perfectly fine for Swift 5.6 and earlier versions:

func validateUUID<R>(using handler: (R) -> Bool) {
// ...
}

let uuids = [String]()
validateUUID { uuids.contains($0) }

However, if the closure has more than one statement, the compiler will complain with the error “Unable to infer type of a closure parameter in the current context”:

func requestUUID<R>(_ handler: (R) -> Result<R, Error>) { }
func notifyDuplicatedUUID<R>(_ uuidRepresentation: R) { }
func processUUID<R>(_ uuidString: R) -> Result<R, Error> { }
let uuids = [String]()
requestUUID { // The error is on this line.
if uuids.contains($0) {
notifyDuplicatedUUID($0)
}
return processUUID($0)
}

The compiler wants our help with type inference even if it could’ve managed without it:

requestUUID { (uuidString: String) in
if uuids.contains(uuidString) {
notifyDuplicatedUUID(uuidString)
}
return processUUID(uuidString)
}

Swift 5.7 will have improvements in closure type inference and will no longer ask for such help, meaning the previous code snippet will compile just fine.

New Types

Swift 5.7 will introduce a couple of new and interesting type families to the standard library. Let’s take a look at them.

Regex

Regex promises to become a new, simple, and powerful way of dealing with regular expressions. As a simple but impressive example, let’s say we need to parse text data into objects of this type:

struct Person {
let firstName: String
let secondName: String
}

Here’s a string to parse, with some unexpected whitespace characters:

let input = "  Leo   Tolstoy"

This is a Regex object, initialized from a literal that will help us deal with the string parsing (pay attention to named capturing groups):

let regex = #/\s*(?<firstName>\w+)\s*(?<secondName>\w+)\s*/#

The following code shows how we can retrieve information from the string using that Regex object:

let match = try! regex.wholeMatch(in: input)
// These are named capturing groups defined above.
let firstName = match!.firstName
let secondName = match!.secondName
let person = Person(firstName: String(firstName),
secondName: String(secondName))

Can you guess what Person the object resulted in? My jaw dropped when I saw that it’s actually Person(firstName: "Leo", secondName: "Tolstoy")! Now processing huge sheets of CSV-formatted text with pure Swift will become a piece of cake.

Clock, Instant, and Duration

These types might not look that inspiring, but they’re still important. Swift has been in need of its own clock-related abstractions to replace wrappers around C code from the Dispatch framework. And at last, it will have them.

The Clock protocol will define the concept of passing time. Two main implementations are ContiniousClock, which runs no matter what, and SuspendingClock, which doesn’t advance if the process is suspended. So, Clock determines how exactly time runs:

try await Task.sleep(until: .now + .seconds(1),
clock: .continuous)

The aforementioned .now + .seconds(1), despite its resemblance to the DispatchTime syntax, is the instance of the new type Instant. Its purpose, as you may have guessed, is to represent a moment in time.

Another use case for the Clock type is measuring the time it takes for code blocks to execute. The resolution of such a measurement is claimed to be suitable even for benchmarks:

let timeElapsed = ContinuousClock().measure { benchmarkMe() }

The type of the resulting timeElapsed is not TimeInterval, it’s an instance of another new type called Duration. The type is Comparable, has all necessary arithmetic operations defined, and, in general, is ready to humbly serve.

Concurrency

Of course, the next Swift release cannot do without additions to the new concurrency model that was introduced a couple of updates back. This time, we’ll have all sorts of groovy little door prizes.

Concurrency on the Top Level

Currently, such calls are not permitted on the top level (for instance, in the root of the main.swift file):

await doAsyncWork()

With Swift 5.7, it will become perfectly legal.

Concurrent Function Execution Fixes

Another improvement in the concurrent world of Swift is that non-isolated asynchronous functions will now always run in the global concurrent pool instead of in an isolated context, from where they’re called. Consider this code snippet:

func doHeavyComputations() async {
// ...
}

actor Computer {
func compute() async {
await doHeavyComputations()
}
}

Prior to Swift 5.7, doHeavyComputations might start running on the Computer actor, blocking it unnecessarily. Starting with Swift 5.7, it will always execute concurrent functions in their own context. Although the improvement is mostly invisible, it’s rather important for code performance.

Unavailability from a Concurrent Environment

We’ll be able to mark functions as not available in an asynchronous context. For example, the code below won’t compile, resulting in the error “Global function 'doSomethingSensitive' is unavailable from asynchronous contexts; Use the asynchronous counterpart instead”:

@available(*, noasync, message: "Use the asynchronous counterpart instead")
func doSomethingSensitive() {
// ...
}
func soAsynchronousThings() async {
doSomethingSensitive() // The error is here.
}

Since we often write code that is just not meant to be used in multi-threaded environments, this new annotation will help avoid concurrency-related problems like race conditions in such code areas.

distributed actor

Swift 5.7 will introduce a new type of actors—distributed actors, as opposed to regular, local ones. At first, the keyword distributed will just enable more checks on the call site: Non-distributed members will only be available from within the scope of the actor, and distributed members will become implicitly async and throwing.

Unfortunately, at the moment of this writing, the current development snapshot of Swift 5.7 and the public beta version of Xcode 14 crashed on compiling distributed members of actors.

According to the proposal, non-distributed members of distributed actors can only be called from the actor’s isolated context. From the outside, only distributed members can be called. They will be also implicitly asynchronous and throwing in order to expose potential I/O and network operations that might be lurking inside. And finally, all those involved in distributed members’ types (i.e., types of arguments and returned values) must conform to Codable.

Here’s an example of a distributed actor:

distributed actor Person {
var name = "Leo Tolstoy"
distributed func changeName(_ name: String) {
self.name = name
}
}

Calls to the name property from outside the Person type won’t compile, whereas calls to changeName(_: String) will compile only when marked with try and await. These measures are aimed to increase the thread safety of distributed systems.

Opaque Types

Opaque types, introduced ages ago along with the SwiftUI, hide information about real underlying types and allow you to describe types in terms of the protocols they implement. Swift 5.7 will add a few important things to the syntax of opaque types.

First of all, they will be allowed as function arguments. This, in particular, will allow our SwiftUI compositions to be more concise:

func viewWrapping(_ wrapee: some View) -> some View {
// ...
}

Currently, you can only define the function above using a more clumsy, generics-based construction:

func viewWrapping<V: View>(_ wrapee: V) -> some View {
// ...
}

Opaque types will be also allowed as return values inside structural types:

func viewsWrapping(_ wrapee: some View) -> [some View] {
// ...
}

Existential Types

Existential types, just introduced in the last release, will expectedly evolve as well, moving Swift farther from generics-based code. As an example, consider the following snippet that uses good ol’ protocols with associated types (a.k.a. PATs):

protocol ServiceSupplier {
associatedtype SuppliedService
var service: SuppliedService { get }
}
protocol BackendService { }
struct ServiceCoordinator<Supplier: ServiceSupplier>
where Supplier.SuppliedService == BackendService {
let supplier: Supplier
}

After the next language release, we’ll be able to add one or more so-called primary associated types to protocols using a syntax similar to the generic clause:

protocol ServiceSupplier<SuppliedService> {
associatedtype SuppliedService
var service: SuppliedService { get }
}

And why would you want to do this? Because it enables us to use ServiceSupplier as an existential type, instead of a generic constraint:

struct ServiceCoordinator {
let supplier: any ServiceSupplier<BackendService>
}

Primary associated types are going to be added to many types in the standard library (most notably, to the Collection protocol), which will allow us to use them as existential types as well:

let numbers: any Collection<Int> = [1, 2, 3]

Low-Level Additions

And finally, all lovers of messing with raw pointers and other low-level stuff will have a bunch of additions to the standard library to play with. For example, currently the language doesn’t provide a way of loading data from arbitrary, unaligned sources like binary files. Swift 5.7 will bring this method to deal with the problem:

let data = dataSource.withUnsafeBytes {
$0.loadUnaligned(fromByteOffset: 128, as: YourType.self)
}

Swift 5.7 will also allow us to compare pointers using simple operators like &lt;= without type conversions.

The family of UnsafeRawPointer types will obtain methods to get pointers to the previous or next alignment boundaries using methods like this:

let next = current.alignedUp(for: UInt8.self)

It may not look as exciting as playing with new concurrency possibilities, but undoubtedly, this will have its own important areas of application.

Conclusion

All the above is most likely not everything that will be included in the next Swift release, but it’s definitely the bulk of it. Just as expected, the concurrency model keeps evolving, as well as existential types, which means they are here to stay.

I’m personally glad to see more improvements for using opaque types, and, of course, I can’t wait to start using the shorthand syntax of optional binding.

In the meantime, to improve the code quality of the software you’re developing, check out Shipbook. It gives you the power to remotely gather, search, and analyze your user logs and exceptions in the cloud, on a per-user & session basis.