[HN Gopher] Multitasking, parallel processing, and concurrency i...
___________________________________________________________________
Multitasking, parallel processing, and concurrency in Swift
Author : ingve
Score : 60 points
Date : 2024-06-10 07:55 UTC (1 days ago)
(HTM) web link (eclecticlight.co)
(TXT) w3m dump (eclecticlight.co)
| behnamoh wrote:
| Tangent: I love Swift's new typed throws! One frustration with
| Python is you can't--just by looking at function signature--tell
| that it raises exceptions. With Swift (and afaik, Java) you would
| write it in the function definition, but still, you wouldn't know
| what _type_ of exception to handle. Now that problem is solved!
|
| More generally though, I wish we could avoid having exceptions to
| begin with. What's the reason behind their prevalence in almost
| every language?
| animal_spirits wrote:
| We couldn't let go of GOTO :)
| jiehong wrote:
| In Java, typed exceptions are now considered an antipattern,
| because it caused problems in multiple parts of the language
| such as in lambdas. Results from rust tend to be more
| composable.
| jkubicek wrote:
| Do you know of any blogs or writing on this that I could
| read?
|
| 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.
| MBCook wrote:
| I've heard that claim many times before. I'm a daily Java
| developer, and can provide a little perspective on what
| Java is like. Though I don't know the exact specifics of
| the claim.
|
| 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.
| koito17 wrote:
| Minor nitpick: OutOfMemoryError and StackOverflowError
| are subclasses of Error, not Exception. It is an anti-
| pattern to catch any Error subclasses since these are
| usually JVM-internal exceptions. Programs are firmly in
| undefined behavior territory when they e.g. catch an
| OutOfMemoryError and do nothing with it. This is why
| catching Throwable is considered a bad idea compared to
| Exception. In the majority of cases, there is no
| reasonable recovery from a thrown Error, unlike a thrown
| Exception.
|
| 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.
| MBCook wrote:
| Ah that's right. I'm used to RuntimeException since
| that's what people subclass to get out of the checked
| exception mess. The exact hierarchy around rare things
| like OutOfMemory just isn't something that ever comes up
| in day to day work so it's easy to forget.
|
| Thanks for the correction.
| kccqzy wrote:
| It's not a type system loophole in the sense that the type
| system is supposed to contain subtyping as a feature. In
| Java you could be unhelpful and pass every argument as type
| Object. (Of course the language allows you to cast the type
| afterwards.) That's not the fault of the type system it's
| the people using it. Do you hear people proposing to ban
| the use of Object in function arguments? No? Because it's a
| style issue not a type system issue.
| MBCook wrote:
| You're right but I think that's pretty self-defeating so
| people wouldn't really do it.
|
| 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.
| Tainnor wrote:
| In my opinion, exceptions should be for really exceptional
| situations - things you can't realistically expect and/or
| which just signal a programming error (e.g. a precondition
| being violated). They aren't usually recoverable locally
| and should bubble up the stack, so you can handle them at
| some boundary (e.g. so you can serve up a 500 page to a
| request without crashing the entire server for everyone).
|
| 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.
| svieira wrote:
| I haven't heard anyone anywhere say that "typed exceptions
| are an antipattern" just "typed exceptions _as implemented
| by Java_ are an antipattern". The general problem is well
| set forth by Anders Hejlsberg, Bill Venners and Bruce Eckel
| in _The Trouble With Checked Exceptions_ [1]. Briefly,
| typed exceptions in Java are not genericizable. Everything
| must be known at compile time. That is to say, you cannot
| write: 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/
|
| [3]: https://www.ponylang.io/
|
| [4]: https://ocaml.org/manual/5.2/effects.html
|
| [5]: https://koka-lang.github.io/koka/doc/index.html
| sc22 wrote:
| You _can_ write in Java that very interface you claimed
| could not be written. 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.
| Someone wrote:
| > 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.
|
| 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?
| 9dev wrote:
| But isn't the actual advantage here that it gives me the
| ability to handle that error case specifically? I mean
| stuff like IO errors can happen everywhere at any time
| anyway, but if I need to do something specifically if
| there is no such element, isn't that a plus? Otherwise,
| I'd have no chance to handle that error if I can do so.
| MBCook wrote:
| Handling there is probably the right thing to do.
|
| 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.
| rahkiin wrote:
| Also nice: a throw in Swift is like a return! It is setting the
| exception in a specific register and then returns. The 'throws'
| signature indicates to the caller two things: the user needs to
| use some exception handling, and the callsite needs to handle
| the special return register in case it is filled with an
| exception
|
| This way you do not need to do expensive stack walking
| dwaite wrote:
| Typed throws (of errors, swift does not support exceptions) are
| for pretty specific usage scenarios - there was a years-long
| recurring discussion around adding them to the language (which
| I was an opponent).
|
| 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.
| jayd16 wrote:
| >What's the reason behind their prevalence in almost every
| language?
|
| 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.
| troupo wrote:
| > What's the reason behind their prevalence in almost every
| language?
|
| 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://e
| rlang.org/documentation/doc-4.9.1/doc/design_princi...)
| eikenberry wrote:
| IMO the problem with tools that are great "when used
| correctly" is that if they don't force that "correctly" part
| or the feature works in such a way that people just fall into
| correctly due to path-of-least-resistance, then people don't
| use it correctly. This, again IMO, is why people have
| problems with Exceptions. It is that they don't have these
| qualities and they are almost universally used incorrectly...
| thus the new languages have eschewed them much like they
| eschewed heavy handed OO abstractions. They were tried and
| found to be lacking for their intended purpose and
| alternatives are being tried.
| MBCook wrote:
| Right, that's the Java issue. It becomes very easy to just
| add "throws Exception" all over the place to silence
| warnings when things start to get tricky.
|
| 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.
| MBCook wrote:
| I'm not familiar with Rust, but Swift has three options and
| it sounds like it may be similar.
|
| 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.
| nomel wrote:
| An exception allows you to handle exceptional errors at any
| level, without having to handle errors, or _write a single line
| of code_ , at every other level. You can assume perfection, and
| put that catch at the level of the abstraction where there's an
| actual concern for the exceptional, keeping all the other code
| simple.
|
| 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.
| betimsl wrote:
| Everything old is new again.
___________________________________________________________________
(page generated 2024-06-11 23:01 UTC)