The power of Result types in Swift
swiftbysundell.com
swiftbysundell.com
You can then centralize the error handling by having a "stderr"-like output on your filters.
When you have a call/return architectural style, you need to return something, and thread the result along, making things more complicated.
That is an important root insight. It also applies to for loops, and could apply to switch statements
switch result { case something: break case none: error: empty: default: //? }
Values are much easier to reason about and inspect than functions, so it's better to have simple functions that return complicated values than to complicate the functions themselves.
And you know this how?
> Values are much easier to reason about and inspect than functions
Right, which is why we want to avoid functions as much as possible.
> simple functions that return complicated values than to complicate the functions themselves
Fortunately, these filters aren't functions, so this doesn't complicate them, it makes them simpler.
Professional experience.
> Fortunately, these filters aren't functions, so this doesn't complicate them, it makes them simpler.
Functions are at least restricted enough that you can sort of reason about them. If these things are not even functions then there's no hope of ever understanding them.
- (void)fetchData whenDone finished: [a block] ifFails failed: [another block]
Or: - (void)fetchData whenDone finished: [a block that has nullable Data and nullable error as parameters]
Both approaches had problems. The first one uses two result blocks, but you want to do a lot of the same stuff usually, like:* Stop the spinner
* Enable the send button again
This often lead to repetitive code or a maze of function calls.
The second one made you basically duck type for the kind of response it really was. Does it have an error? It probably failed. Does it have data? It probably worked. But what if it has both data and an error (error data?) or simply both values were nil? Brittle code.
While I really like the railroad approach the typical problem area Swift is applied to (often web API driven iOS and macOS applications) simply don't lead to these huge chains of functions. I'm definitely going to try it for backend code though.
Also, compare it to the interface of Futures.
I agree you don't see them collected together, as a rule, but if you follow Results<> like this in those domains you see them being used multiple times on the backend before being yeilded to the frontend for further handling, resulting in chains that are just spread out a little bit more.
Personally I've started exposing them through JSON at the API level to further extend the systems explicitness and idempotency.
Using exceptions is dealing with errors out-of-band. Using Result types is dealing with errors in-band. In-band is somewhat less flexible because you don't have the power to arbitrarily alter the execution flow of your program. Out-of-band is much easier to reason about by that very token.
With legacy style result callbacks, you’ll have both an optional value and an optional error returned - this corresponds to four possible states. If I’m using an API returning this, I have to think about two cases which probably won’t happen (neither an error or value, or both an error and a value). I’d like to think the API I’m using wouldn’t return those states, but since it’s not typed as such then code I’m writing should probably handle those cases gracefully.
By using a result type as per the article, it is well defined that only two states are possible. This pattern is a really nice one, and is a great use of Swift’s associated value enums.
(Likely applicatives will take another decade to go mainstream.)
Erring[ResultType] typealias Handler1 = (Result<Data, LoadError> -> Void)
typealias Handler2 = (Result<Address, IndexError> -> Void)
func load(addr: Address, then handler: @escaping Handler1) { ... }
func lookup(name: string, then handler: @escaping Handler2) { ... }
// What would the type of Handler3 be?
func loadDefault(then handler: @escaping Handler3) {
lookup("default") { result in
switch result {
case .success(let address):
load(address) { result2 in
switch result2 {
case .success(let data):
// Happy path
case .failure(let error):
// We have a load error for handler3
}
case .failure(let outerError):
// ERROR: We have a lookup error for handler3
}
}
}
// P.S., don't complain about syntax I really don't know swift :P
Handler3's type is actually the sum of LoadError and IndexError. This is sort of a non-trivial problem for a lot of use cases of Either. Monadic and Railway Either programming typically solve this problem by saying, "Fine well then every single Either/Result in the chain needs to be of the same type, which includes a unified error type." In this article's case, I guess we could appeal to Swift's binding to FoundationKit to lift up NSError? Sure! That'll compile! Maybe there is a Compound Type thing? It will probably compile but it'll shape your handler in weird ways.It still seems pretty unsatisfying. The point here was to make it easy to work out what errors we should handle, but now we need an explosion of combination types (unique to each call graph in this case, wow!) Modern Haskell, which along with ML pioneered these style of, has some slick type machinery tricks to deal with this that are not well supported yet (even type lists are not a slam dunk because we need something that's order-insensitive). Most folks will be required to make a new ad-hoc sum type (like Result, but EitherError<Error1, Error2>), and then have to pattern match on that to handle errors.
But at the end of the day, even Haskell with all its principle has try/catch statements in IO and an exception hierarchy. In fact, it's considered best practice for production code to avoid giving the appearance that Either can capture all your errors (since it seldom can): https://www.fpcomplete.com/blog/2016/11/exceptions-best-prac...
Which is not to say Either's (and Railway programming's) "Sum-type errors") aren't a substantial improvement over special return values or Common Lisp/Go's product-style return values. They are. But sadly they can't really get you away from exception handling when you start interacting with the "real" world as the examples here do. It's a win for things like HTTP responses or decoding libraries.
After doing that, the type of Handler3 can be had as Result<Tuple<Data, Address>, NSError>, for example. And how do you combine the two results into the Tuple? That's an applicative, I'm sure swift provides combinators to do just that.
The problem comes because you have to either use a single error for two parts or deal with a tough explosion of types. No?
But as I've said, yeah you can do this. But it's unfortunate because it destroys a lot of information that's useful in the type signature. Haskell does this with exception hierarchies and some clever runtime casting attempts, but even that has ergonomic issues.
What'd be really amazing is if we could get the totality checking of pattern matching but also the composability of exception handlers, and that's what the next generation of error handling primitives are trying to do.
The combination of data and lookup is more like data(lookup(default)), which in haskell (assuming we could encode the error type neatly):
load =<< lookup "default".
So Result3's first parameter is of type Data.> You probably don't really care about the exact error that can be thrown by either handler, you just need enough information to display a sensible message to the user.
Please consider looking for the part of the article we're discussing where it says, "However, adding that extra type information to our result type does have some nice benefits - for example, it lets us specifically handle all possible errors at the call site, like this"
Ultimately we'd like to get to where the compiler can do at least a rudimentary totality check on the error handler.
P.S., I know my prose is rambling and my code is overly verbose, but please consider how it makes me feel when you give me suggestions I myself discussed in the prior post.
I hear this parroted a lot, and I really don't get it. When an exception happens, you can't resume control flow (unless you're in Common Lisp, which has an objectively better system for exceptions than any we use). In that case, you're right that specific type and details don't matter.
But if you get the failure value back as part of the control flow with a precise type then you can actually recover from it. People quote the Erlang philosophy out-of-context suggesting this is Actually Always Bad. It's not! You might want to: provide a default value for a server (bind to 127.0.0.1 or 0.0.0.0), synthesize a default templated configuration file, propose a valid algorithm over a forbidden one in a cryptographic negotiation, etc.
> Plain old unchecked exceptions work better. Probably cheaper too, because sum types require wrapping and unwrapping at each step, while exceptions can be very cheap when not thrown.
I'd be surprised if that was the case. Stack unwinding and introspection looking for handlers is infamously expensive whereas reading fields from a struct on the stack is seldom considered very expensive. Even if you do have an on-heap indirection that breaks locality, it's not like firing off a random class handlers further up the stack is gonna be BETTER for locality. Given that `finally` exists and objects have destructors, an exception up-stack is often a lot more code than it looks like.
I'm not sold on resumable exceptions. It makes more sense to pass default values directly. Have you seen this bit in Stroustrup's book: http://www.cpptips.com/term_except
It'll still be cheaper to do a simple pattern match into a struct for complex handlers, but for depth 1 they should essentially be identical. That's a very interesting tradeoff for the Swift compiler!
Or are they just two different problem domains?
One good thing about this is not having to push down anything to do with logging or reporting into subcomputations.
/s :D
Types are a tool just like auto spell checking. Why not leverage them to reduce cognitive load?
Furthermore, i would expect a well behaved dev to avoid c at all cost.
https://www.cvedetails.com/product/47/Linux-Linux-Kernel.htm...
Nowadays, everything is done at a high level using some kind of pre-existing framework and the type of code being written is cookie-cutter so none of this applies. Swift belongs to this new age trend in coding,
If there is anything new about Swift, it is a return to the past of proper programming languages.
"Oh, it was quite a while ago. I kind of stopped when C came out. That was a big blow. We were making so much good progress on optimizations and transformations. We were getting rid of just one nice problem after another. When C came out, at one of the SIGPLAN compiler conferences, there was a debate between Steve Johnson from Bell Labs, who was supporting C, and one of our people, Bill Harrison, who was working on a project that I had at that time supporting automatic optimization...The nubbin of the debate was Steve's defense of not having to build optimizers anymore because the programmer would take care of it. That it was really a programmer's issue....
Seibel: Do you think C is a reasonable language if they had restricted its use to operating-system kernels?
Allen: Oh, yeah. That would have been fine. And, in fact, you need to have something like that, something where experts can really fine-tune without big bottlenecks because those are key problems to solve. By 1960, we had a long list of amazing languages: Lisp, APL, Fortran, COBOL, Algol 60. These are higher-level than C. We have seriously regressed, since C developed. C has destroyed our ability to advance the state of the art in automatic optimization, automatic parallelization, automatic mapping of a high-level language to the machine. This is one of the reasons compilers are ... basically not taught much anymore in the colleges and universities."
-- Fran Allen interview, Excerpted from: Peter Seibel. Coders at Work: Reflections on the Craft of Programming
See chapter 13 example BCPL programs:
http://www.cl.cam.ac.uk/~mr10/bcplman.pdf
See chapter 13 example MCPL programs:
But apart from that: the guy is wrong of course! Working in JavaScript still drives me crazy because I don't have decent autocomplete and no IDE warnings I'm using an API wrong.
The desire to remain ignorant of the problems (by not using tooling which can reveal it) is remarkably popular, however. Enjoy your bliss!