Swift Evolution Monthly: March + April '23

1-Year Anniversary: Summaries on GitHub! AsyncStream, Attached Macros (in SwiftPM), Package Publish, Parameter Packs, Feature Flags & Foundation Preview.

Swift Evolution Monthly: March + April '23
Photo by freestocks / Unsplash

It’s already been a whole year since I started writing this newsletter! 🥳 Can you believe it? I’ve had so much fun sharing all of the latest news and updates about Swift with you over the past year. Starting with a recap of the Swift Evolution history in the first issue of the newsletter, I managed to cover as many as 44 proposals already, and with this issue, I'm adding six more, reaching 50!

To celebrate this milestone, I’ve created a GitHub project with all my past summaries shared with this newsletter already. This should make it much easier for anyone in the community to find a specific proposal summary. I designed the repo in such a way that you can simply replace apple in a proposal's GitHub URL (https://github.com/apple/swift-evolution) with FlineDev and you’ll directly get to the summary of the related proposal!

I also decided that starting with this newsletter, I will also always summarize one old proposal discussed and accepted before this newsletter was even kicked off that I find still relevant and interesting. This way, the repo should become even more useful as a "summary project" for all the sometimes too-detailed proposal documents. And I invite members of the community who already wrote proposal summaries in other places to contribute to the project, too! Or just write a summary for a topic you've always been interested in, writing a summary is a great way to learn more about Swift, I can tell from experience! 😉

Thank you so much for being a part of this journey with me so far. Here’s to another great year of Swift Evolution Monthly! 🍻🎉

Accepted Proposals

The first of the newly accepted proposals highly relates to an older proposal I didn't cover yet. So, I think it's most natural to start with this old proposal:

SE-0314: AsyncStream and AsyncThrowingStream

Links: 📝 Proposal | 💬 Reviews: 1st, 2nd  |  ✅ Acceptance

We all have used many types conforming to the Sequence protocol, like Array, Dictionary, or String. It allows for easy iteration with  for element in array. The same treatment was added to the new Swift Concurrency world with AsyncSequence, which is pretty much the same as Sequence but the values aren't immediately available, instead they come at a later time. But usage looks exactly the same, with only the await keyword added: for await element in array.

Actually, there's one thing wrong with the sample code just provided: array. Because the Array type doesn't conform to AsyncSequence, instead, we need new types that we can instantiate and return in our APIs, or that we can consume from system APIs. And that's where AsyncStream and its throwing counterpart AsyncThrowingStream come into play. If you're not familiar with streams, think of them as "async arrays". This is a simplification of course, and creating an AsyncStream is quite different from an array. Here's a simple example:

let swiftFileURL: URL = ...
let matches: (String) -> Bool = { $0.contains("// TODO:") }

let matchingLineNumbersStream = AsyncStream<Int> { continuation in
   Task {
      var lineNumber: Int = 0
      for try await line in swiftFileUrl.lines {
         lineNumber += 1
         if matches(line) {
            continuation.yield(lineNumber)
         }
      }
      
      continuation.finish()
   }
}

Note that we're being passed a continuation which we pass new values to by calling yield() and when we reach the end of the "async array", we call .finish(). This way we don't have to write a custom iterator logic.

Then, consumers like some rule in a linting tool could use the stream like so:

for await lineNumber in matchingLineNumbersStream {
   violatingLines.append(lineNumber)
}
Iterate an Async(Throwing)Stream like you do with any other Sequence type.

The cool thing about AsyncStream is that it's not only useful for slow APIs, like calling data from a server. But it can also be used when receiving values very fast, faster actually than we can consume them on the call site. AsyncStream automatically buffers values in this case and sends them in the receiving order, so we never skip any values and things work as expected.

SE-0388: Convenience AsyncStream.makeStream methods

Links: 📝 Proposal  |  💬 Review  |  ✅ Acceptance

Currently, when creating an AsyncStream, you get passed a continuation parameter into the trailing closure to send (or .yield) values to. In some cases, this works fine. But in many situations, you actually might want to pass around the continuation to some other places. And this proposal adds that capability:

let (stream, continuation) = AsyncStream.makeStream(of: Int.self)

await withTaskGroup(of: Void.self) { group in
   group.addTask {
      // code using `continuation`
   }
   
   group.addTask {
      // code using `stream`
   }
}

Note the makeStream(of:) method returning a tuple containing the continuation. It's also worth noting that this is making use of @backDeployed(before:) introduced recently in SE-0376 to make this API available for projects targeting older OS versions than the ones Swift 5.9 will be included in. 💯

SE-0389: Attached Macros

Links: 📝 Proposal  |  💬 Review  |  ✅ Acceptance  |  🔮 Vision

This is a continuation of SE-0382 which introduced "Expression Macros". While the latter focused on a more function-like structure that we could "call" from anywhere using #, this proposal focuses on "attaching" behavior to existing declarations like functions or types using @. This might remind you of SE-0258 which introduced property wrappers that allowed attaching extra behavior to stored properties like done by SwiftUI with the likes of @State. And, in fact, this similarity is on purpose because, to some extent, "attached macros" are an extension of property wrappers to all kinds of declarations and are explicitly stated as "subsuming some of the[ir] behavior".

For example, you could declare a "peer macro" named AddCompletionHandler:

@attached(peer, names: overloaded)
macro AddCompletionHandler(param: String = "completionHandler")

Then attach it to any async API like this (overriding the default param name):

@AddCompletionHandler(param: "completion")
func fetchImage(url: URL) async throws -> Image { ... }

The macro would create an overloaded version of the same function with a non-async completion-handler as a parameter for you, for example:

func fetchImage(url: URL, completion: (Image) -> Void) throws {
   Task {
      let image = try await self.fetchImage(url)
      completion(image)
   }
}

The implementation of a macro is a bit involved and requires an understanding of swift-syntax & a new SwiftSyntaxMacros module. But the mere possibility to adjust our Swift code using Swift code is very powerful.

In total, there will be 5 different kinds of attached macros:

  1. Peer Macros:
    Attached to declarations to provide additional declarations.
  2. Member Macros:
    Attached to types to add new members (like properties).
  3. Accessor Macros:
    Attached to stored properties to turn them into computed properties with full access to the local context (unlike property wrappers).
  4. Member Attribute Macros:
    Attached to types to apply attributes to all members (similar to @objcMembers or @MainActor).
  5. Conformance Macros:
    Attached to types to add new protocol conformances.

I really liked how Swift improved the developer experience by automatically synthesizing the conformance to types like Codable (SE-0166 + SE-0295), Hashable, or Equatable (both SE-0185). And I'm really happy to see that we are now getting the power of implementing similarly magical APIs ourselves. 🪄

SE-0391: Package Registry Publish

Links: 📝 Proposal  |  💬 Review  |  ✅ Acceptance

This proposal defines all things missing to allow for a unified way of publishing Swift packages to registries like the now officially Apple-backed Package Index:

  • A metadata format for related URLs, authors & organizations.
  • A package signature format for registries that require signing.
  • A new package-registry publish subcommand to "create a package source archive, sign it if needed, and publish it to a registry" all in one go.
  • A set of requirements for registries supporting signing to ensure security.

The good news is that you probably won't have to learn more about the details of this proposal thanks to the great work of Dave & Sven, plus the support of the Swift community, including these 85 amazing people. All that framework authors might need is the new publish subcommand, maybe not even that unless they use a CI for uploads, cause Apple might ship a UI for it in Xcode. 🤞

SE-0393: Value and Type Parameter Packs

Links: 📝 Proposal  |  💬 Review  |  ✅ Acceptance

This proposal adds two new keywords to the Swift language:

  • each: Precedes a type (like some) & marks it as a "parameter pack".
  • repeat: Precedes a type/expression & "expands" the related pack.

But what is a "parameter pack" and what does it mean to "expand" one?

In Swift when writing functions that accept a variable number of values of (potentially) differing types, the best we can do if we don't want to erase their types is to write a generic function like this:

func areEquivalent<A, B, C>(_ first: A, _ second: B, _ third: C) -> Bool

// example usage
areEquivalent("42", 42, 42.0)  // => true

While this function uses generics so we don't have to write many overloads to the same function with different type specifications, it is restricted to taking exactly three parameters. But what if we wanted an areEquivalent implementation for any number of differing types? To support up to 10 parameters, we would need to define 10 different generic methods like the above, each with a growing number of generic types. But couldn't we somehow tell Swift to do that for us?

Well, that's exactly what this proposal does by means of each and repeat:

func areEquivalent<each T>(_ values: repeat each T) -> Bool

A "parameter pack" is what allows a function to accept an arbitrary number of arguments of any type, here: each T. The "expansion" of a pack is the process of unpacking its contents into individual arguments, here: repeat each T. The single function declaration above is "expanded" to function declarations with zero, one, two, three, and more parameters of (potentially) distinct types by Swift.

And that's not all, there's much more to this proposal – it's actually quite detailed. The above is just a short intro to parameter packs, but it doesn't even show the implementation side of it. Here's an implementation from the proposal:

func zip<each T, each U>(firsts: repeat each T, seconds: repeat each U) -> (repeat (each T, each U)) {
  return (repeat (each firsts, each seconds))
}
Multiple parameter packs in one function are possible but are inferred to have the same size.

And this seems just the first step into the larger topic of variadic generics programming, a related pitch & a related proposal are listed further below.


Want to see your ad here? Contact me at ads@fline.dev to get in touch.

SE-0394: Package Manager Support for Custom Macros

Links: 📝 Proposal  |  💬 Review  |  ✅ Acceptance  |  🔮 Vision

With SE-0382 "Expression Macros" & SE-0389 "Attached Macros" (⬆️), we're getting closer to a Swift world full of macros. But what if we wanted to share macros across projects or with the community in an open-source package?

This proposal adds a new target type to the Swift package manifest named macro which has the same shape as other target types like target or testTarget. But it behaves more like the plugin target type added in SE-0303: When included in a project, the macro targets are built as executables so that the compiler can run them as part of the compilation process for any targets which depend on them. Also, like package plugins, the macro target will be executed in a sandbox that prevents file system and network access.

I'm looking forward to the macros the community will come up with, especially if Apple introduces how to use these new features to a wider audience at WWDC.

Proposals in Progress

Recently Active Pitches/Discussions

Other Developments Worth Mentioning

I had already written about the Upcoming Feature flag with which you can try out Swift 6 features in SE-0362. Soon after I wrote this very popular article about how you can use some Swift 6 features today, Swift.org received a very related improvement: Now you can filter proposals with an upcoming feature flag and you'll even see the feature flag's name right within the list:

In December 2022 Apple announced that they would be open-sourcing Foundation (or rather rewriting Foundation in the open). Less than 5 months later, they already announced that a preview of this "future of Foundation" is available, focusing on FoundationEssentials and Internationalization. Contributions are welcome! Time to test your apps with it and report bugs.

The second vision document ("A Vision for Macros") has been accepted and is now included right within the Evolution GitHub repo in the visions folder.

And that's all for March & April. Until next time!

💁🏻‍♂️
Enjoyed this article? Check out my app RemafoX!
A native Mac app that integrates with Xcode to help translate your app.
Get it now to save time during development & make localization easy.
👨🏻‍💻
Want to Connect?
Follow me on 👾 Twitch, 🎬 YouTube, 🐦 Twitter, and 🦣 Mastodon.