Multitasking, parallel processing, and concurrency in Swift
eclecticlight.co
eclecticlight.co
More generally though, I wish we could avoid having exceptions to begin with. What's the reason behind their prevalence in almost every language?
They are useful within your own module, e.g. when they aren't part of an exported API, as an alternative to other error handling methods.
You can support them in generic utility methods like map, because you will just rethrow the typed throw.
Otherwise, it is meant for systems programming or use in embedded environments - basically the 'leaf code' that doesn't have upstream variability or independent upstream dependencies. It is meant to indicate things like 'errno' in a POSIX API. I actually feel it is a poor functional fit there; the valid error results differ both across errno-setting functions and across UNIX implementations of a particular function. The symbolic assignment (semantic error code to errno numeric value) is also variable across implementations. However, this allows the compiler to know errors are an int-sized stack type, vs a witness of a heap-allocated object.
To look at it differently, errors are meant either for recovery or for indicating a general failure for code to attempt to clean up from. Once you delegate an error to other code by rethrowing it, you no longer are conveying proper knowledge for recovery - so there's no purpose to having it be typed; all you can really do is try to fail gracefully.
A specific example - something like Swift Data is a poor fit for typed throws, because the database layer itself is adaptable. My Application doesn't know whether a failure is due to the local disk being out of space or a transient network issue connecting to a remote server, and attempting to recover from these in my application code is a pretty bad pattern because I'm baking in assumptions about a particular configuration of Swift Data across my application code (or into applications which use my module).
My opposition to the feature is that it is never required (you can indicate failure in the return signature via something like Result), adds overall language complexity, and is likely to be misused in cases where it doesn't provide value - it just adds ABI complexity over simply documenting expected Error types.
This way you do not need to do expensive stack walking
Unexpected errors are inescapable when you consider OOMs and other things so its almost required to support that. Ending scopes and bubbling up an error without a ton of unwrap boilerplate is actually really compelling.
Exceptions became a way to capture diagnostic context for where an error happened and a (limited) view of state, so a lot of Java server applications use them as part of their issue reporting and diagnosis process.
Other languages may expect say the application to halt on unexpected errors, and to build tools to locally evaluate the core dump (such as crash reports and symbolication in Apple-land).
This does have some influence on developers in terms of the ramifications of a failure (failed request vs stopped application/server), so it can have interesting effects in how seriously a team evaluates potential errors and designs for recovery/cleanup.
Naively, untyped exceptions in Swift always felt like an obvious type system loophole that we should be closing as soon as possible.
I trust that the "typed exceptions are an antipattern" people know what they're talking about, but I really don't understand the reasoning behind that position.
In Java a given method either doesn’t throw any exceptions, or only throws the named ones and their children. So nothing, or maybe just InvailArgumentException (and its descendants).
However there’s a lot of different kinds of exceptions. So very quickly you tend to end up with one of two things. Functions have a big list of possible exceptions (an A, or B, or C, or F, or G) or the far more common in company code: throws Exception. Don’t bother to list stuff, just say “I throw stuff”.
Except, that’s all a lie. Because every function implicitly can throw RuntimeException and its children. Things like OutOfMemory or StackOverflow.
So basically all code everywhere realistically has to be aware of exceptions. A lot of people get lazy and don’t bother to list the actual kinds of exceptions they throw or pick one that’s a runtime exception so they don’t have to add the ‘throws’ to the signature to every single method.
And how do you solve calling a method with a checked exception? Well the right thing to do is use a try block, but so often people just add it to their own signature and pass the buck.
So a lot of the benefits just sort of get lost.
Swift now has three options: doesn’t throw, throws something, throws of type X. You can’t have multiple types. And you can’t pretend the exception doesn’t exist and just let it bubble up, you have to put a ‘try’ on the statement line to do that.
As someone who works in both languages Swift’s approach does seem like an improvement. I’m not sure how big.
But just in Swift, being able to explicitly state the one type of the exception that may be thrown seems like a very nice add for type safety and reducing boilerplate checks.
With that said, it is true RuntimeException and its subclasses are unchecked exceptions. Thankfully, those exceptions tend to be things like IllegalArgumentException, NullPointerException, etc., which are still pretty serious but not as bad as the two examples given.
Thanks for the correction.
Despite Thread.stop() being deprecated, it is still used in things like application servers to deal with user code which is not completing in a specified timeframe.
> So basically all code everywhere realistically has to be aware of exceptions. A lot of people get lazy and don’t bother to list the actual kinds of exceptions they throw or pick one that’s a runtime exception so they don’t have to add the ‘throws’ to the signature to every single method.
Not just that, there are features like lambdas where the core language features were not designed with exceptions in mind. There are third party libraries which don't have allowances for proper failure cases. So you will have code adapted to wrap their exceptions into runtime exceptions for interoperability.
Of course, there's no guarantee the code which did not provide an interface which allowed for exceptions will not have unexpected side-effects when runtime exceptions or errors are being thrown.
> However there’s a lot of different kinds of exceptions. So very quickly you tend to end up with one of two things. Functions have a big list of possible exceptions (an A, or B, or C, or F, or G) or the far more common in company code: throws Exception. Don’t bother to list stuff, just say “I throw stuff”.
Ideally, someone creating an API thinks about how the others utilizing code will accomplish recovery or graceful failures, and craft an API to account for this. Unfortunately, the errors are often handed up, which really only accounts for broad failure behavior and corresponding cleanup (such as failing the corresponding HTTP request with a generic 500 error).
The heavy use of adapter patterns in Java means that the errors being handed up may be specific to the configuration, such as failures coming from the network layer of a database backend. It is crucial for the adapter interfaces to think about failure scenarios and recovery, but it unfortunately doesn't happen often very often.
So in most cases, Java should just throw Exception, because the only way a developer can really attempt recovery is to break encapsulation and operate based on knowledge of the inner workings of the object they were handed.
The reasoning is that they force a contract upon implementations that they may not be able to implement in spirit.
Let’s say a library defines a container interface that says a method can throw a NoSuchElementException.
Now, I implement that interface to implement a disk backed container. What do I do when that code encounters an IOException? Swallow it (very bad), throw an NoSuchElementException, possibly wrapping the IOException inside it, if possible (highly confusing, as now the class changed the meaning of NoSuchElementException), or throw an unchecked exception that wraps it instead?
Most Java code would pick that last option, and you end up with a nice interface that says that a call can throw NoSuchElementException, but callers must be prepared to get a RuntimeException that wraps any other kind of exception.
That makes the implementation adhere to the interface, but only in name.
(Note that the writers of the interface, the implementation of the disk-backed container, and the user of that container often all will be different teams at different organizations)
So, why declare that typed exception, in the first place?
I think the argument is that there is probably a better way of expressing that in code than exceptions. Perhaps returning an Either<Data, ErrorCode> kind of value.
There’s also an argument that that’s not truly an exception because it should be expected that an I/O error is a reasonable possibility. Things like OutOfMemory or VirtualMachineError are more what they may be thinking exceptions should be saved for. Truly unexpected things.
For things that are expected to go wrong sometimes, algebraic data types (including Result types) are better. They are part of the type system, so everything you can do with types, you can do with them, including making functions parametric over whether they return an "error" or not. Exceptions need extra mechanisms so that functions can be parametric over them (e.g. Swift has "rethrows", Java has... nothing). They aren't really composable with anything else.
interface ThrowingRunnable<E extends Throwable> {
void run() throws E;
}
Instead, the best you could do is manual multiplication of interfaces: interface AThrowingRunnable {
void run() throws ExceptionA;
}
interface BThrowingRunnable {
void run() throws ExceptionB;
}
// ... snip thousands more ...
Or give up and erase all the type information by saying `throws Exception` or `throws Throwable`.Genericizing throws in particular was tried in Midori [2] and worked out really well (by report). In addition, several less-than-completely-obscure languages are starting to experiment with the algebra of effects in general (as opposed to error handling in particular). Pony [3], OCaml [4], and others are experimenting with bringing what Koka [5] (among others) to the masses.
[1]: https://www.artima.com/articles/the-trouble-with-checked-exc...
[2]: https://joeduffyblog.com/2016/02/07/the-error-model/
interface ThrowingRunnable<E extends Throwable> {
void run() throws E;
}
Only, hardly anybody writes it that way, so people forget that it's possible. Why don't people use exception polymorphism in that way? I am not sure, but I believe it would make code too verbose, since every function containing a virtual method call to something that might throw would have to be parameterized that way.But there’s a pretty good argument that the way exceptions are implemented in Java makes the easiest thing to do simply be to start tacking “throws Exception“ on the end of tons of methods once you start to have trouble. Everywhere I’ve worked you start to see that in chunks of the code base.
You don’t have to. It’s certainly avoidable. But as things get more and more complicated people will just opt for that to save themselves time. Java’s design unintentionally encourage it when the going gets rough. Even when using an IDE that can add the correct types to the list for you, that’s what people reach for.
I've always worked strongly in the physical world side of software, like network stacks, test equipment, robots, etc, so I can't see a sane alternative, that doesn't involve in increasing the LOC by 20%, and being error prone (it's trivial to accidentally eat an exception if you're relying on returned values).
If you don't want to bubble up the exception to the user, that's trivial too. You just catch it at whatever level you choose, handle whatever, then return something nice.
Swift is getting typed throws now because the untyped ones require memory allocation, and so aren't suitable for embedded programming.
Because nobody really wants low-level stuff like divide by zero, memory errors, index out of bounds wrapped as a `Result type ? Both Rust and Go have panics. Every function would effectively return a `Result` type since any non-constant expression can hypothetically return an error.
More often than not it's not the caller that is responsible for handling errors/exceptions.
When you force the caller to take care of every single error, you end up with unreadable boilerplate code which hides the actual logic. There's a reason why Rust ended up with the `?` syntax sugar.
On top of that exceptions will occur. You can't pretend they won't and kill the app if they do. Again, even Rust and Go ended up adding handlers for their brain-dead panics.
Exceptions (when wielded correctly) end up simplifying your program. You develop for the happy path (mostly), and let code at the higher level of hierarchy make decisions about unhappy paths. That's how you get Erlang's supervision trees (https://erlang.org/documentation/doc-4.9.1/doc/design_princi...)
Because Swift forces the keyword ‘try’ (or a variant) before calling code that may throw it ends up being a lot of work to try to avoid the issue just by making everything throw. It’s much easier to do the correct thing and just handle the error in a smart place.
In Swift code that throws when called must have “try” in front of it, making it really obvious where that’s going on. Your three options:
try - calls the code and either returns the like normal or an error that you’re forced to handle in a catch.
try? - calls the code and returns the value or nil (null value) if an error is thrown.
try! - calls the code and returns the value. If the function throws an error your app panics.
It’s quite nice. You can choose to handle the error when you need to. If you don’t really care about the specifics of the error and just want to treat any kind as a failure try? cleans up your code.
And try! lets you avoid writing boilerplate when you know it’s impossible for the error to be thrown but the compiler can’t deduce that from the source alone.
If only it was a problem to begin with ;)
The article doesn't even really explain the modern push - the difference between threads and fibers, or between actors and concurrency domains, or most critically between x86 and ARM in their memory models.
Would someone please write that article?
libdispatch source code is an enlightening read for those interested in diving deeper into the C Runtime powering Swift concurrency.
There was also the Power Macintosh 9500/180MP back in 1996, equipped with two 180 MHz PowerPC 604e CPUs.
MacOS didn't support multi processors natively (it couldn't schedule different programs on different CPUs or anything), so it was only useful as basically a "Photoshop accelerator"