@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 is nice, clean code, and it certainly looks like it serves as a good showcase for the benefits of asynchronous coding with async/await and asynchronous sequences built on top of async/await.
Or does it?
Here is the equivalent code 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 does not (and will not) have async/await, but it can nevertheless provide the equivalent functionality easily and elegantly. How? Two features:
- Polymorphic Write Streams
- Messaging
for try await trivial.
Polymorphic Write Streams
In the Objective-S implementation,https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.csv is not a string,
but an actual identifier, a Polymorphic Identifier, adding the ref: prefix turns it into a binding, a first class variable.
You can ask a binding for its value, but for bindings that can also be regarded as collections of some kind, you can also
ask them for a stream of their values, in this particular case a MPWURLStreamingStream. This stream is a Polymorphic Write Stream that can be easily composed with other filters to create pipelines. The linesAfter:
method is a convenience method that does just that: it composes the URL fetcher with a filter that converts from bytes to lines of
text and another filter that drops the first n items.
Objective-S actually has convenient syntax for creating these compositions without having to do it via convenience methods, but I wanted
to keep differences in the surrounding scaffolding small for this example, which is about the for try away and do:.
When I encountered the example, Polymorphic Write Streams actually did not have a do: for iteration, but it was trivial to add:
-(void)do:aBlock
{
[self setFinalTarget:[MPWBlockTargetStream streamWithBlock:aBlock]];
[self run];
}
(This code lives in MPWFoundation, so it is in Objective-C, not Objective-S).
Those 5 lines were all that was needed. I did not have to make substantive changes to the language or its implementation. One reason for this is that Polymorphic Write Streams are asynchrony-agnostic: although they are mostly implemented as straightforward synchronous code, they work just as well if parts of the pipeline they are in are asynchronous. It just doesn't make a difference, because the semantics are in the data flow, not in the control flow.
Messaging
The other big reason an asynchronousdo: was easy to add is messaging.
If you focus on just messaging -- and realize that a good metasystem can late bind the various 2nd level architectures used in objects -- then much of the language-, UI-, and OS based discussions on this thread are really quite moot.One of the many really, really neat ideas in Smalltalk is how control structures, which in most other languages are special language features, are just plain old messages and implemented in the library, not in the language.
So the for ... in loop in Swift is just the do: message sent to a collection, and the
keyword syntax makes this natural:
for event in lines {
...
}
...
lines do: { :event |
...
}
Note how making loops regular like this also makes the special concept of "loop variable" disappear. The "loop variable" is just the block argument. And I just realized the same would go for a not-nil result of a nil test.
Anyway, if "loops" are just messages, it's easy to add a method implementing iteration to some other entity, for example a stream, the way that I did. (Smalltalk streams also support the iteration messages).
And when you can easily make stream processing, which can handle asynchrony naturally and easily, just as convenient as imperative programming, you don't need async/await, which tries to make asynchronous programming look like imperative programming in order to make it convenient.
