FREE TRIAL: Accelerate your app development career with Hacking with Swift+! >>

How to manipulate an AsyncSequence using map(), filter(), and more

Paul Hudson    @twostraws   

AsyncSequence has implementations of many of the same methods that come with Sequence, but how they operate varies: some return a single value that fulfills your request, such as requesting the first value from the sequence, and others return a new kind of async sequence, such as filtering values as they arrive.

This distinction in turn affects how they are called: returning a single value requires you to await at the call site, whereas returning a new async sequence requires you to await later on when you’re reading values from the new sequence.

To demonstrate this difference, let’s try out a few common operations, starting with a call to map(). Mapping an async sequence creates a new async sequence with the type AsyncMapSequence, which stores both your original async sequence and also the transformation function you want to use. So, instead of waiting for all elements to be returned and transforming them at once, you’ve effectively put the transformation into a chain of work: rather than fetching an item and sending it back, the sequence now fetches an item, transforms it, then sends it back.

So, we could map over the lines from a URL to make each line uppercase, like this:

func shoutQuotes() async throws {
    let url = URL(string: "https://hws.dev/quotes.txt")!
    let uppercaseLines = url.lines.map(\.localizedUppercase)

    for try await line in uppercaseLines {
        print(line)
    }
}

This also works for converting between types using map(), like this:

struct Quote {
    let text: String
}

func printQuotes() async throws {
    let url = URL(string: "https://hws.dev/quotes.txt")!

    let quotes = url.lines.map(Quote.init)

    for try await quote in quotes {
        print(quote.text)
    }
}

Alternatively, we could use filter() to check every line with a predicate, and process only those that pass. Using our quotes, we could print only anonymous quotes like this:

func printAnonymousQuotes() async throws {
    let url = URL(string: "https://hws.dev/quotes.txt")!
    let anonymousQuotes = url.lines.filter { $0.contains("Anonymous") }

    for try await line in anonymousQuotes {
        print(line)
    }
}

Or we could use prefix() to read just the first five values from an async sequence:

func printTopQuotes() async throws {
    let url = URL(string: "https://hws.dev/quotes.txt")!
    let topQuotes = url.lines.prefix(5)

    for try await line in topQuotes {
        print(line)
    }
}

And of course you can also combine these together in varying ways depending on what result you want. For example, this will filter for anonymous quotes, pick out the first five, then make them uppercase:

func printQuotes() async throws {
    let url = URL(string: "https://hws.dev/quotes.txt")!

    let anonymousQuotes = url.lines.filter { $0.contains("Anonymous") }
    let topAnonymousQuotes = anonymousQuotes.prefix(5)
    let shoutingTopAnonymousQuotes = topAnonymousQuotes.map(\.localizedUppercase)

    for try await line in shoutingTopAnonymousQuotes {
        print(line)
    }
}

Just like using a regular Sequence, the order you apply these transformations matters – putting prefix() before filter() will pick out the first five quotes then select only the ones that are anonymous, which might produce fewer results.

Each of these transformation methods returns a new type specific to what the method does, so calling map() returns an AsyncMapSequence, calling filter() returns an AsyncFilterSequence, and calling prefix() returns an AsyncPrefixSequence.

When you stack multiple transformations together – for example, a filter, then a prefix, then a map, as in our previous example – this will inevitably produce a fairly complex return type, so if you intend to send back one of the complex async sequences you should consider an opaque return type like this:

func getQuotes() async -> some AsyncSequence {
    let url = URL(string: "https://hws.dev/quotes.txt")!
    let anonymousQuotes = url.lines.filter { $0.contains("Anonymous") }
    let topAnonymousQuotes = anonymousQuotes.prefix(5)
    let shoutingTopAnonymousQuotes = topAnonymousQuotes.map(\.localizedUppercase)
    return shoutingTopAnonymousQuotes
}

All the transformations so far have created new async sequences and so we haven’t needed to use them with await, but many also produce a single value. These must use await in order to suspend until all parts of the sequence have been returned, and may also need to use try if the sequence is throwing.

For example, allSatisfy() will check whether all elements in an async sequence pass a predicate of your choosing:

func checkQuotes() async throws {
    let url = URL(string: "https://hws.dev/quotes.txt")!
    let noShortQuotes = try await url.lines.allSatisfy { $0.count > 30 }
    print(noShortQuotes)
}

Important: As with regular sequences, in order to return a correct value allSatisfy() must have fetched every value in the sequence first, and therefore using it with an infinite sequence will never return a value. The same is true of other similar functions, such as min(), max(), and reduce(), so be careful.

You can of course combine methods that create new async sequences and return a single value, for example to fetch lots of random numbers, convert them to integers, then find the largest:

func printHighestNumber() async throws {
    let url = URL(string: "https://hws.dev/random-numbers.txt")!

    if let highest = try await url.lines.compactMap(Int.init).max() {
        print("Highest number: \(highest)")
    } else {
        print("No number was the highest.")
    }
}

Or to sum all the numbers:

func sumRandomNumbers() async throws {
    let url = URL(string: "https://hws.dev/random-numbers.txt")!
    let sum = try await url.lines.compactMap(Int.init).reduce(0, +)
    print("Sum of numbers: \(sum)")
}
Hacking with Swift is sponsored by Essential Developer

SPONSORED From August 2nd to 8th you can join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer!

Save your spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.