4 ms·
The tricky part of this approach comes when you extend it. Let's say we have two functions and we'd like to combine them and return a third. typealias Hand
by KirinDave 8y ago
The tricky part of this approach comes when you extend it. Let's say we have two functions and we'd like to combine them and return a third.
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-practices-haskell 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.
- wereHamster 8y agoYou can improve that code by having a single error type. Either by returning the same error from both handlers or having an error sum type which contains both errors. 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. NSError may be just enough for that purpose! 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.
- taeric 8y agoDidn't the post you are responding to say that? I suspect you didn't read past the example. The problem comes because you have to either use a single error for two parts or deal with a tough explosion of types. No?
- masklinn 8y agoThere are known solutions to this problem: you can type-erase to a single supertype (e.g. Error/NSError[0]) or convert from the third-party's type to a first-party type[1] for instance. [0] https://developer.apple.com/documentation/swift/error https://developer.apple.com/documentation/swift/error [1] https://boats.gitlab.io/failure/ https://boats.gitlab.io/failure/
- KirinDave 8y agoIt's sort of amazing that you're replying to a post suggesting something I suggested, by re-suggesting what all 3 posts prior to you suggested. 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.