On the proliferation of try, and soon, await (2020)
forums.swift.org
forums.swift.org
> Functions that only mutate instances of well-encapsulated types through their public APIs. That includes nearly all functions that are not methods.
> Methods of well-encapsulated types that only mutate non-member instances of other well-encapsulated types through their public APIs.
This is incorrect! Consider the following function (in psuedocode):
func insert(id: Int, object: ThingInSpace) {
ids.update(id);
var location = get_location(object);
locations[id] = location;
var metadata = compute_metadata(object);
metadatas[id] = metadata;
}
If compute_metadata or get_location throw, then even though we're only modifying dictionaries or sets through their public APIs, we can still invalidate an invariant that affects our program correctness. This function is safe if and only if get_location and compute_metadata cannot throw. If they can throw, this should be rewritten as func insert(id: Int, object: ThingInSpace) {
var location = get_location(object);
var metadata = compute_metadata(object);
ids.update(id);
locations[id] = location;
metadatas[id] = metadata;
}
Adding "try" to the original function makes it clear that this function can destroy the invariant of ids, locations, and metadatas, and it needs to be rearranged to be the second function in order for the code to be correct.Under the rules laid down by the original poster in the link, functions which only modify well-encapsulated types via their public APIs cannot violate invariants.
My code shows that there are "invariant[s] that must be externally enforced" even in code that only deals with well-encapsulated types via their public APIs. And if you don't have explicit error handling, it isn't obvious that the invariant can be broken.
… of those well-encapsulated types. Throwing an exception like this cannot break an internal invariant of the list object which is what the author is saying.
You can’t break the list object which sounds crazy by modern standards but in languages without this kind of encapsulation (like C) this can and does happen.
The proposal is to keep the "try" keyword but allow functions to explicitly opt out so that "try" is not needed on every function call which can throw. The author suggested a "throws_anywhere" keyword after the argument list. My proposal would be to add "try blocks" which have the effect of allowing an exception to be rethrown anywhere inside the block (i.e. within a "try block" you can omit the normal "try" on a function call). The pattern:
try {
do_something();
do_something_else();
}
would be equivalent to: try do_something();
try do_something_else();
So if you wanted the effect of "throws_anywhere" you just wrap the whole body of the function in one of these blocks. But you could also wrap just part of a function, for example just the part where the invariants are maintained, and require explicit handling in the parts where invariants are temporarily broken.And there's certainly no reason for `insert` to re-throw its helpers' internal errors. What would its caller do with them?
func insert(id: Int, object: ThingInSpace) {
var location = try get_location(object);
ids.update(id);
locations[id] = location;
metadatas[id] = compute_metadata(object);
}
Now I can be sure that compute_metadata doesn't fail, so it's fine that I've inlined the call, whereas it's important that get_location isn't inlined. But this syntax also suggests to a code reviewer that maybe insert should wrap the error returned by get_location and caught.Of course you could always catch the error and restore the state, but without the `try` syntax, you can't tell by reading the function which lines can fail, and where you need to handle the error.
Here, Swift (it seems) makes an implicit suggestion that a developer keep their error handling tight and bounded. If you don't, the language softly penalizes you with the need to add keywords to be explicit about changed flow. Go does something similar in eschewing try / catch error handling for a single panic / recover system and very explicit error handling via return values (this was a very intentional design to address what the language creators perceived to be common failure modes in code they were maintaining at the time in other languages, see here [https://go.googlesource.com/proposal/+/master/design/go2draf...] for details). In Go, it's harder to make a certain category of mistake, but at the cost of the user writing more code to handle the error path (and assuming the implicit cost that every line of code could introduce a different mistake).
Every language feature in every language we use (static type declaration vs. implicit types with casting, variable predeclaration vs. implicit creation on first write or read, GOTO vs. function calls vs. try/catch vs. continuation passing, etc.) makes these tradeoffs.
The solution to the problem with to have errors be dynamically typed (a trait in Rust, an interface in Go, etc.) and insist that callers be ready for an unknown error type to bubble up.
They bump up against a reality of exceptions in a bad way: once you have a system for throwing exceptions, the set of types your code must handle becomes fundamentally unbounded because any of your dependencies can change at any time. If there are no exceptions and you have a function-call control flow, you can enforce that the only types the caller understands from the callee are the types in the signature. If your language supports thrown exceptions, then at the site of a function call, the value resulting from the call can be the values in the signature or any exception that can be generated by any function called by the callee (and in general, without control of the entire code stack, that set is unknowable).
That's a fundamental truth of thrown exceptions, and trying to constrain it by putting the thrown type in the signature left us in the state we're in now... People just throw and catch very abstract exceptions because they'll have to handle those exceptions anyway, since it's impossible to guarantee a dependency won't try to throw them.
This has historically resulted in either ignoring errors from sub-functions, because their error return type don't map properly, or returning generic "i got an error" without any more information about what the error was.
And all this was historically patched over by having verbose log files that could be written to from anywhere and the user had to peruse and try to make sense of after the fact.
Some franework tried to create complicated error code (like Windows COM) but in the end there is no free lunch. Errors are hard.
i) There's RuntimeException which is an unchecked exception which could happen anywhere, so even the absence of a throws is not a guarantee of not throwing.
ii) In order to avoid many different throws specifiers, you end up with wrapping exceptions to a smaller set of exceptions (e.g. JNDI may throw a NamingException that wraps an IOException)
iii) IDEs offer to put try-catch { // todo } to avoid compiler errors. Developers often stop thinking then, where the right thing is more often to let it throw - e.g. I've seen FileNotFound being caught and logged (at debug) and not exposed to a caller, so a UI doesn't see it, with bad results.
This is the thing that always pissed me off. The default catch block should be to wrap it in a RuntimeException and rethrow it.
... a scan of GitHub and feeding keywords and constructs into a couple hoppers would be all the ML you needed for that. ;)
(... I'm joking, but if you're a language designer, this is actually not a terrible idea for figuring out how people use your language "in the wild." What you do with that information will separate wisdom from knowledge).
asset1 = await httpGET(...)
asset2 = await httpGET(...)
from: assets = await Promise.join(httpGET(...), httpGET(...))
asset1 = assets[0]
asset2 = assets[1]
Where the first does one request and doesn't start the next until the previous had completed, and the second does them concurrently.BUT - you could have a language where async functions are awaited by default, and you'd have to write something like:
assets = Promise.join([pending httpGET(...), pending httpGET(...)])
Where pending means don't await, and absence of it when invoking an async function does require it.Presence of an 'async' decorator on functions isn't strictly needed, but does convey extra information in the API, which is useful.
I disagree. It's akin to having a `gc` decorator on a function that uses GC. Maybe useful for C++, but not useful for high-level languages like Java, Swift, Go, JavaScript, Python.
Long-term, I firmly believe `async` will reveal itself to be a mistake. It's simply unwillingness by language designers (or more likely implementors) to treat threading like GC - "that magic is OK, but this magic needs to have extra syntax".
Go chose a different route and treats all runtime magic as implicit behaviour (well, almost all - goroutines can block in loops - but they're trying to fix that, or maybe they have already). Java (project Loom) is going down the same path. It's only a matter of time Swift, Python etc. realize their mistake.
Now just remove `async` and `throws` function annotations and/or make those default, and all noise will be removed!
[1] https://github.com/apple/swift-evolution/blob/main/proposals...
There is a technical reason behind this, it's not just style.
The async-await transformation is highly compatible with calling other things in the C environment and receiving callbacks from that environment.
In some implementations the async-await transformation converts nested async functions to stackless state machines that are compatible with the C environment assumed by other libraries (especially in other languages) and by the operating system.
But it does have some performance cost, compared with functions that can assume stack scope. That cost only appears when using functions labelled "async".
Making every function automatically async in this model is nicer syntax (imho) but adds that cost to every function, unless the compiler is able to do a more global analysis, or deviates from strong compatibility with the C environment.
Another approach is to transform them to system threads. Then you don't need the async-await transform and it's compatible with the C environment, but you lose performance in some types of program where async-await is used, and sometimes a lot of memory or address space. So there's still a cost. Especially on targets where threads aren't particularly well implemented.
Yet another is green threads or other C-compatible coroutine implementation. This costs some compatibility with the C environment on some targets, though. Switching stacks in C works almost everywhere, but not everywhere, and has portability issues. The standard library function to assist with stack switching has been removed from POSIX, but it was never particularly fast anyway.
Go gets around this by being willing to deviate more from the C environment. It goes hand in hand with the very self-contained nature of Go compiled programs. Java gets around this by being a JIT interpreter which is free to do a lot of things its own way inside the Java execution.
asset1 = httpGET(...)
asset2 = httpGET(...)
doSomethingWith(asset1)
doSomethingWith(asset2)
No problem, both requests concurrent, don't have to think about it. They will probably be neatly batched by the executor into a single outgoing TCP packet too. asset1 = httpGET(...)
asset2 = httpGET(functionOf(asset1))
doSomethingWith(asset2)
Second request has to wait until the first has completed, because this is correct. It can't be started until the first is completed.I see a lot of desire to purge await from languages but I find the explicit control necessary. What would UI code look like with implicit yields?
Here is a post from Kotlin's language designer on their philosophy: https://elizarov.medium.com/how-do-you-color-your-functions-...
I think that just shows how necessary some kind of explicit syntax is, despite all the talk I see that it's unnecessary. I would love to see more examples though, or a Kotlin native UI.
> I would love to see more examples though, or a Kotlin native UI.
Jetpack Compose is a Kotlin-native UI framework. Here are some examples of how it uses coroutines: https://developer.android.com/jetpack/compose/kotlin#corouti...
Console and ASP net core have null as their SynchronizationContext.
Which results in the majority of async code, which doesn't run in a UI context, needing to spam `.ConfigureAwait(false)` everywhere. It's the most visible wart in C# today - and no end of requests for a more succint alternative: https://github.com/dotnet/csharplang/discussions/645
Seems like a clear way to add it to the method signature and inform the caller and solve the issue with minimal work. Does that not work?
i.e. the same async method could be called from UI code and should resume on the UI thread immediately afterwards - or it could be called from library code - the method doesn't know that (nor does the person who wrote the method).
I do agree that I do not want to see it, though.