Why Apple makes Concurrency on Swift (AsyncSequence)

Introduction


When developing applications for the Apple ecosystem, such as iOS, macOS, and tvOS, Swift has established itself as the predominant programming language of choice. Despite its origins as an open-source language, Swift has gradually evolved into a domain-specific language tightly integrated with Apple's platforms. In recent years, particularly with the introduction of version 5.5, Swift has undergone significant changes that have further enhanced its capabilities.



Concurrency: A New Frontier


Concurrency, in the context of programming, involves managing the execution of multiple tasks simultaneously. Swift, being a language deeply ingrained in Apple's ecosystem, recognizes the importance of efficiently handling asynchronous and parallel code. Asynchronous code is designed to be suspended and resumed at a later point, with only one segment of the program executing at a given time.



Traditionally, Swift used closures to manage asynchronous code. Closures, akin to lambdas in Python or JavaScript's block functions, were the go-to mechanism for handling asynchronous operations. Additionally, developers could turn to delegate functions or utilize frameworks like Combine and RxSwift to achieve similar results.



var asyncClosure: (() -> Void) = {
    print("Code Executed!")
}

/// At a certain point
asyncClosure()



// Combine: Swift's first-party reactive framework
// Surprisingly similar to RxSwift

var asyncPublisher = PassthroughSubject<Void, Never>()
asyncPublisher
    .sink({  _ in
        print("Code Executed!")
    })
    .store(in: &subscription)





However, with the introduction of Swift's new Concurrency system, a fresh approach is available in the form of AsyncSequence.


var asyncPublisher = PassthroughSubject<Void, Never>()

var asyncStream = AsyncStream<Void> { continuation in
    continuation.yield(())
    continuation.finish()
}

for await action in asyncStream {
    print("Code executed")
}

for await action in asyncPublisher.values {
    print("Code executed")
}




While the for-in loop has long been used in Swift to iterate through sequences, the for-await-in statement represents a novel way of dealing with asynchronous code, bearing a striking resemblance to the familiar for-in loop.



To understand why Apple introduced the for-await-in statement, we need to take a brief look back at Swift's past, specifically the Swift 2 environment. Two critical factors influenced the evolution of Swift: Erik Meijer's work on Reactive Programming and Swift's Automatic Reference Counting (ARC) memory management system.




Erik Meijer's Influence


Over a decade ago, Erik Meijer, a former Microsoft engineer, pioneered the concept of Reactive Programming and played a significant role in the development of RxJava. Around the same time, the Swift community recognized the need for a similar framework and thus created RxSwift. Meijer's core idea involved reversing the concepts of Iterator and Iterable, allowing for the handling of side effects and asynchronous execution.



In Meijer's view, most programming languages inherently possessed the concepts of Iterator and Iterable, which Swift analogously represented with IteratorProtocol and Sequence. An Iterator is an interface (or protocol in Swift) that defines a `moveNext` function, while Iterable is an interface that produces an Iterator.



struct Iterator<Element> {
    let moveNext: () -> Element
}

struct Iterable<Element> {
    let getIterator: () -> Iterator<Element>
}



extension Iterator {
	func fMap<B>(_ transform: @escaping (A) -> B ) -> Iterator<B> {
		return Iterator<B> { transform(self.moveNext()) }
	}
}

extension Iterable {
	func fMap<B>(_ transform: @escaping (A) -> B) -> Iterable<B> {
		return Iterable<B> { self.getIterator().fMap(transfrom) }
	}
}




Moreover, Meijer introduced the concept of Covariance, which states that if A is a subtype of B (A <: B), then () -> A is a subtype of () -> B. Swift's Iterator and Iterable naturally exhibit this covariance.

Contravariance, on the other hand, flips the relationship: if A is a subtype of B (A <: B), then () -> B is a subtype of () -> A. This concept is particularly relevant when dealing with Observers and Observables.




struct Observer<A> {
    let onNext: (A) -> Void
}

extension Observer {
    func contramap<B>(_ transform: @escaping (B) -> A) -> Observer<B> {
        return Observer<B> { b in
            self.onNext(transform(b))
        }
    }
}

struct Observable<A> {
    let subscribe: (Observer<A>) -> Void
}

extension Observable {
    func fmap<B>(_ transform: @escaping (A) -> B) -> Observable<B> {
        return Observable<B> { observerB in
            self.subscribe(Observer<A> { a in
                observerB.onNext(transform(a))
            })
        }
    }
}




CoVariant
A <: B
() -> A <: () -> B

Iterator<A>: () ->A
Iterable<A>: () -> Iterator<A>
Iterable<A>: () -> (() -> A)


ContraVariant
A <: B
() -> B <: () -> A

Observer<A> : (A) -> Void
Observable<A>: (Observer<A>) -> Void
Observable<A>:(A -> Void) -> Void


In Swift, this concept of reversing Iterable and Iterator allowed for more effective handling of asynchronous code, as exemplified by the usage of RxSwift:


someObservable.rx
    .subscribe({ [weak self] in
        self?.text = "Async happened"
    })
    .disposed(by: disposeBag)





Here, the '[weak self]' notation is a capture list, a mechanism originating from Swift's C-based heritage, and it is used to avoid memory retention issues within the block.




Swift's Memory Management: ARC's Limits


Swift's memory management system, known as Automatic Reference Counting (ARC), plays a crucial role in managing memory allocation and deallocation. ARC automatically tracks how many times a reference to an object is called, and when the reference count reaches zero, it deallocates the memory associated with that object. However, there are situations where ARC doesn't count certain references, leading to the introduction of keywords like "Weak" and "Unowned" in Swift.



// CPP
#include <functional>
#include <algorithm> 

unordered_map<int, int> tans;

int someValue;
sort(tans.begin(), tans.end(), [someValue](auto a, auto b) {
    // Here, we can use 'someValue' in the lambda
    return a.second > b.second;
});




The code snippet above illustrates a common pattern in C++ where a lambda function captures an external variable (someValue in this case) to use it within the lambda's body. Similar to C++, Swift employs capture lists to handle such scenarios. However, Swift's memory management system introduces an additional layer of complexity.


In Swift, the '[weak self]' notation, as shown in a previous example, is a capture list that developers frequently use. Swift is influenced by its C-based heritage, and capture lists are employed to prevent memory retention issues. The '[weak self]' construct ensures that the closure doesn't increase the ARC count of 'self' , preventing memory leaks, even if 'self' is not actively used within the closure.


Despite the additional complexity, Swift's memory management system, combined with the introduction of for-await-in statements in the Concurrency system, significantly improves the handling of asynchronous code. These advancements offer developers a more efficient and less error-prone approach to managing memory and concurrency, ultimately leading to enhanced code quality and maintainability. As the Apple ecosystem continues to evolve, Swift's tools and techniques will evolve in tandem, ensuring a bright future for this versatile programming language.



Back To Concurrency Code


var asyncPublisher = PassthroughSubject<Void, Never>()

var asyncStream = AsyncStream<Void> { continuation in
    continuation.yield(())
    continuation.finish()
}

for await action in asyncStream {
    print("Code executed")
}

for await action in asyncPublisher.values {
    print("Code executed")
}



Concurrency's for-await-in statement no needs to handle with 'ARC-Closure' things





Conclusion


In conclusion, Apple's introduction of the for-await-in statement in Swift's Concurrency system marks a significant step forward in the evolution of asynchronous code handling. This novel approach simplifies code structure and eliminates the need for capturing values within closures, enhancing code readability and maintainability. While Erik Meijer's influence and the intricacies of Swift's memory management system played pivotal roles in shaping this evolution, the result is a more efficient and developer-friendly approach to handling concurrency in Swift. As the Apple ecosystem continues to evolve, so too will the tools and techniques available to Swift developers, ensuring a bright future for this versatile programming language.