While reviewing the WWDC ‘21 session videos, I encountered session on Asynchronous Sequences. The preview displayed code that asynchronously fetched and processed live earthquake information from the U.S. Geological Survey:
` ``` @main struct QuakesTool { static func main() async throws { let endpointURL = URL(string: “https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.csv")!
for try await event in endpointURL.lines.dropFirst() { let values = event.split(separator: “,”) let time = values[0] let latitude = values[1] let longitude = values[2] let magnitude = values[4] print(“Magnitude (magnitude) on (time) at (latitude) (longitude)”) } } }
``` `
This code is well-structured and effectively highlights the advantages of asynchronous programming using async/await and asynchronous sequences based on async/await.
However, is this truly the case?
Consider the corresponding code written in Objective-S:
` ``` #!env stsh stream ← ref:https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.csv linesAfter:1.
stream do: { :theLine | values ← theLine componentsSeparatedByString:’,’. time ← values at:0. latitude ← values at:1. longitude ← values at:2. magnitude ← values at:4. stdout println:“Quake: magnitude {magnitude} on {time} at {latitude} {longitude}”. }. stream awaitResultForSeconds:20.
``` `
Objective-S, which lacks async/await functionality, can still achieve the same outcome with ease and elegance. This is made possible by two key features:
- Polymorphic Write Streams
- Messaging
Let’s explore how these features combine to simplify the implementation of a for try await equivalent.
Adaptable Write Streams
In the Objective-S code, https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.csv is not simply a string but a Polymorphic Identifier. By adding the ref: prefix, it transforms into a binding, a first-class variable. Bindings that function as collections can be queried for a stream of their values. In this scenario, it’s a [MPWURLStreamingStream](https://github.com/mpw/MPWFoundation/blob/master/Streams.subproj/MPWURLStreamingStream.m). This stream, classified as a Polymorphic Write Stream, allows for easy integration with filters to build pipelines. For instance, the linesAfter: method combines the URL fetcher with a filter to convert bytes into text lines, followed by dropping the first n items.
Objective-S offers concise syntax for constructing these compositions without relying on convenience methods. However, I aimed for minimal scaffolding differences in this example to focus on for try await and do:.
Although Polymorphic Write Streams didn’t initially have an iterative do:, adding one was straightforward:
` ``` -(void)do:aBlock { [self setFinalTarget:[MPWBlockTargetStream streamWithBlock:aBlock]]; [self run]; }
``` `
(This code snippet is written in Objective-C, not Objective-S, due to its presence in MPWFoundation).
These five lines were all it took. No major modifications to the language or its implementation were necessary. One contributing factor is that Polymorphic Write Streams operate independently of asynchrony. They are primarily synchronous in their implementation yet seamlessly integrate with asynchronous elements within their pipeline. This is because the semantics are embedded in the data flow, not the control flow.
Messaging
The other key enabler of an asynchronous do: is messaging.
By focusing solely on messaging and recognizing that an effective metasystem allows for late binding of object architectures, many language-, UI-, and OS-related debates become irrelevant.
Smalltalk’s ingenious approach treats control structures, typically specialized language features, as regular messages implemented in the library, not the language itself.
Therefore, Swift’s for ... in loop is essentially a do: message sent to a collection, with keyword syntax enhancing its intuitiveness:
` ``` for event in lines { … } … lines do: { :event | … }
``` `
Note that treating loops as ordinary messages eliminates the need for a distinct “loop variable” concept. The block argument assumes this role. Similarly, the result of a nil test, if not nil, could follow the same principle.
If “loops” are merely messages, adding an iteration method to other entities, such as streams, becomes simple, as demonstrated. (Smalltalk streams also support iteration messages).
When stream processing, inherently capable of handling asynchrony, can be made as user-friendly as imperative programming, the need for async/await diminishes. Async/await aims to mimic imperative programming’s style for asynchronous operations to enhance convenience, but this becomes redundant when stream processing offers a natural and straightforward alternative.