Sunday, February 21, 2021
On the State of Swift (Early 2021) or: Awaiting Async
Although my day job is (usually) programming in Python, I have been experimenting with Swift for a few years now. I am neither a Mac nor an iOS developer; these days I’m almost exclusively a backend developer. It should come as no surprise then that I’m most interested in Swift as a potential backend, server-side language. As such, I think I have a somewhat different perspective from many other developers using Swift. Even compared to others interested in server-side Swift, I think I’m outside the norm, as most members of that community that I’ve encountered have been iOS developers who would also like to program the backend for their apps in the same programming language as they use in the apps themselves.
My dalliances with Swift have always come in fits and starts. For me, the language has long been alluring but elusive: I really admire the concepts of the language and its potential but trying to use it outside of the Apple ecosystem has frequently proven very frustrating. That was especially true in the early days, because of the shortcuts that Apple had taken to get Swift out the door.
A Brief Digression on Apple’s Shortcuts
Swift was first released in 2014, as a proprietary programming language available only for Apple’s platforms. Later in 2015, Apple released it as open source, and released a Linux port at the same time. The Linux port, in particular, made it clear that Apple had taken some shortcuts with the initial implementation and release of Swift. Specifically, Swift’s standard library — the built-in functionality that comes with the language — was astonishingly small. It basically consists of just the built-in data types like Int
, Double
, Array
, and Dictionary
. Most modern, high-level languages have standard libraries that are far larger. Python’s standard library, for example, includes modules for dates, data parsing, and networking from low-level socket handling to higher-level HTTP serving. But if Swift’s standard library didn’t include any of those things, how were application developers for Apple’s platforms already able to write apps in Swift?
In turned out that on Apple’s platforms, Swift is intimately connected to the runtime environment of Apple’s previously preferred language, Objective-C. Apple’s messaging around this indicated this was to enable seamless interoperability between Swift and Objective-C code but it also meant that Swift code had ready access to Apple’s existing rich set of libraries written in Objective-C. Notably among these libraries is the Foundation framework — a fairly extensive collection of frequently-needed functionality that most of us developers take for granted.
On Linux, however, Swift is entirely separate from the Objective-C runtime and does not use Apple’s system libraries. To Apple’s credit, Swift on Linux was not left entirely in the lurch; there is a Swift-native version of Foundation. The oldest commit1 dates to 2012, before Swift had even been publicly announced. This version of Foundation, however, “is a substantial reimplementation of the same API” as the old Objective-C version of Foundation and as of this writing, it remains incomplete.2 Earlier versions of Swift’s Foundation implementation were pretty rough with missing functionality and even some incompatibilities with the Objective-C version. Today, however, I tend to think that it is, generally speaking, sufficiently complete and compatible as to be generally useable.
Foundation represents what most developers would consider essential functionality for a programming language. It, together with libdispatch and XCTest, constitute Swift’s Core Libraries; libraries considered so essential that they are distributed with Swift itself on non-Apple platforms. That Foundation, in particular, is now useable on non-Apple platforms means that Swift has passed what was previously a significant hurdle to its adoption on Linux.
Swift Today
The Swift project has ambitions well beyond Apple’s platforms. As stated on the Swift homepage, the “goal of the Swift project is to create the best available language for uses ranging from systems programming, to mobile and desktop apps, scaling up to cloud services.” Swift’s utility for systems, mobile, and desktop programming is evident from it’s use by Apple for Apple platforms. So where does Swift stand now for server-side use? Apple, after all, doesn’t really have a server platform3.
It took some time after the open source release of Swift, but in late 2016 the Swift project announced the formation of the Swift Server Work Group, sometimes abbreviated as SSWG. The SSWG’s remit is to create “APIs [that] provide low level ‘server’ functions as the basic building blocks for developing server-side capabilities”. The first fruit borne of the SSWG’s efforts was SwiftNIO, a non-blocking network I/O library. Since then, the group has sponsored a number of useful libraries.
It’s honestly a little hard for me to believe, but before SwiftNIO’s initial release in 2018, each of the early web frameworks in Swift — specifically, Kitura, Perfect, and Vapor — implemented their own low-level networking interfaces. No wonder, then, that SwiftNIO has been enthusiastically adopted, even by those frameworks that previously did that work themselves.
There’s just one problem. SwiftNIO, as denoted by the “N” in its name, is non-blocking. Swift, however, doesn’t have built-in support non-blocking (often referred to as asynchronous) operations, so this achieved by means of a coroutine engine embedded in SwiftNIO itself. Without syntax for asynchronous code in the language itself, however, using SwiftNIO becomes a tedious exercise in callback hell (a.k.a. the pyramid of doom).
Unfortunately, this isn’t just a problem for direct uses of SwiftNIO; anything built on top of SwiftNIO will necessarily inherit the need to incorporate the callback style of coding. The Swift-based web framework Vapor is a prime example of the contagious nature of SwiftNIO’s callback hell. Vapor 4 is built on top of SwiftNIO, including the database layers. As a result, any database operations in Vapor require the use of callbacks. Take, then, this example of a Vapor 4 web request handler to update a row in the Acronym
table:
func updateHandler(_ req: Request) throws -> EventLoopFuture<Acronym> {
let updatedAcronym = try req.content.decode(Acronym.self)
return Acronym.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound)).flatMap { acronym in
acronym.short = updatedAcronym.short
acronym.long = updatedAcronym.long
return acronym.save(on: req.db).map {
acronym
}
}
}
Notice the return type is wrapped in EventLoopFuture
. Notice that there are multiple return
statements, starting on only the second line of code. Notice the use of flatMap
and map
to set the callback function (and notice that you must figure out which is appropriate in certain situations as they are subtly different). Notice the deep nesting. None of this code interacts with SwiftNIO directly but with other libraries that do.
The good news is that a solution is in the works. Other languages have adopted the keywords async
and await
to resolve the syntactic oddities of asynchronous code and the Swift community has resolved to follow suit4. Swift Evolution Proposal SE-0296, which includes these keywords, has been accepted and implemented. It remains unclear when this feature will be released properly; it is included in Swift version 5.4 currently in beta but is not enabled by default5. Even after Swift itself is updated, SwiftNIO will need to be updated to adopt it, followed in turn by the web frameworks like Vapor. Work on SwiftNIO’s update has started but of course it cannot be released until Swift itself is ready. When the updates to Swift, SwiftNIO, and Vapor are all released, the example above could look something like this:
async func updateHandler(_ req: Request) throws -> Acronym {
let updatedAcronym = try req.content.decode(Acronym.self)
let acronym = await Acronym.find(req.parameters.get("acronymID"), on: req.db)
.unwrap(or: Abort(.notFound))
acronym.short = updatedAcronym.short
acronym.long = updatedAcronym.long
await acronym.save(on: req.db)
return acronym
}
No nested callbacks. Only one return
statement at the end of the function. The return type for the function doesn’t have to be wrapped in EventLoopFuture
. No trying to sort out the difference between flatMap
and map
. But for the use of the async
and await
keywords, it looks just like synchronous code.
I understand why SwiftNIO was written as an asynchronous library; that is where the entire industry is going. But shoehorning an asynchronous library into a synchronous programming language is awkward, to say the least. In my own humble opinion, there’s just not much point to trying to use SwiftNIO and the frameworks built on top of it right now. Relying on nested callbacks is error-prone, difficult to read, and difficult to debug. More to the point, asynchronous code that uses the async
and await
keywords is so much better that all asynchronous code in Swift will inevitably have to be rewritten as soon as they are available. I’d really, really like to start using Swift for server-side code right now but I just don’t feel like I can until async
and await
are available.
-
The commit message on that commit ends with the line,
Swift SVN r3405
which would seem to indicate that perhaps the GitHub repository was created by importing a previous Subversion repository. It is possible that older commits may have been lost or truncated in the process. ↩ -
It is unclear at this point how much of the remaining unimplemented API will ever be completed. Foundation dates back to the OpenStep partnership between NeXT and Sun Microsystems of the early ’90s but was, in turn, built from earlier antecedents dating to the earliest days of the NeXTSTEP operation system in the late ’80s. There is undoubtedly cruft that isn’t really still needed in a modern context. Many components of Foundation are also superseded either by the Swift standard library or by other libraries for Swift like SwiftNIO. ↩
-
Yes, I know. It doesn’t count. ↩
-
The
async
andawait
keywords are, in fact, just one component of a larger, comprehensive suite of concurrency-related proposals for Swift. See also SE-0297: Concurrency Interoperability with Objective-C, SE-0298: Async/Await: Sequences, SE-0300: Continuations for interfacing async tasks with synchronous code, SE-0302:ConcurrentValue
and@concurrent
closures, as well as the forum post outlining the overall Swift Concurrency Roadmap. ↩ -
Support for the
async
andawait
keywords can be activated with the command line flags-Xfrontend -enable-experimental-concurrency
. I haven’t tested to see if it actually works. ↩