I've seen this referred to as "railway programming", and there's a wealth of explanation over at https://fsharpforfunandprofit.com/rop/. In particular, the slide deck from the FP eXchange contains a diagram (slide 75 out of 154) with a happy path along the top, and a sad path along the bottom, and at every stage when you `bind` a Result into an existing Result, you have the opportunity to divert into the sad path. Neat little metaphor.
We're planning on using a similar strategy to ROP in Dark (https://darklang.com). My suspicion is that editor support for "happy path programming" can make a huge difference in how easy it is to write safe code.
What I found interesting is that if you have a pipe/filter architectural style, your happy path stays completely free of error handling, because when a filter doesn't have a good result, it just doesn't pass any data to the next filter in line. Done!
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.
>> if you have pipes/filters, you stay free of error handling: when a filter doesn't have a result, it just doesn't pass any data to the next filter in line.
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: //?
}
I find this concept of a "stderr output" far more complicated. What's next, a "controlling terminal"?
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.
> 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.
It's not exactly the same as railway programming as in Swift it's typically not used to connect functions together but rather to replace separate callbacks with one "result" function and to hard type certain results. One typical case in Objective-C style API's would be:
- (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.
> web API driven iOS and macOS applications) simply don't lead to these huge chains of functions
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.
Your phrasing ("I refuse to see…") makes me very apprehensive of trying to explain, but I'll bite.
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.
This only pushes the responsibility down lower in the stack. Somewhere someone would have to handle a state where we have data and error being nil at the same time.
No it doesn't. This article is specifically about how you can use the Swift type system to prevent that from happening. You get a compile time guarantee that the result will either be data or error.
Not necessarily. The way that the enumerated type is declared, the associated types of the success and failure values are declared to be normal (not optional). This restricts nil from being passed in as the data or the error.
Actually, it puts the responsibility higher in the stack. The first error effectively terminates the computation, so subcomputations can ignore them and the caller can decide what to do with them.
One good thing about this is not having to push down anything to do with logging or reporting into subcomputations.
Wouldn't that be the day. The funny part is how they preached 'errors are values', and instead of giving a good error monad, they implement a poor version of it explicitly where they feel like it.
While typing is a good way to reduce programmer errors, specially for large scale programs, it's much like autocomplete in a word processor. It's cool to have but also a bad habit to form. The quality of prgrams would improve if such aids were reduced and the programmer was expected to form good programming habits instead.
Well, this article is about Result types in Swift,
and my point is, I think this kind of thing is overkill.
It tends to make the learning curve for a new programmer steeper because there's all these extra bells and whistles provided by the language that have to be learnt before you can get going, and then you have sample code and tutorials and so on. Learning and using C was and is a lot simpler than say Swift.
This is completely true. However, after having learned both sides, I certainly appreciate the compiler’s help. I think it’s the classic tradeoff between specialization and accessability at play here.
I think the designers (Apple) have targeted a certain class of apps with Swift, XCode and the underlying libraries (Cocoa). A majority of their existing developer base will likely benefit from what they've done. But from the point of view of new developers and someone who's just trying to use the hardware in an effective way, this is not a step forward.
It's a matter of getting good at it and that takes time and effort, but the end result is far better than not getting good at it and using a more automated, higher level lanuggae to take the easy way out
Some people develop good coding techniques, like Donald Knuth. If you're new, you set out on your own or read up on what they've done or get lucky and work with a good programmer. That's how you pick it up. But, it's all pedal to the metal. Knuth's entire work is for C.
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,
So you are stating no one on Linux kernel development is a match to Donald Knuth, and given the CVE track record they should improve themselves to Donald Knuth level.
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
I'm not trying to glorify Knuth or downgrade Linux kernel developers. I'm only saying that you can leanr a lot from a good programmer, and an apprentice type arrangement is probably the best way to learn programming and we need more of that than new languages
I understand what you're saying, and I agree to some extent. I'm not sure if these kinds of types are actually making things easier or more difficult for learning the underlying language. There's something to be said for stopping at a certain point, or taking more caution with "kitchen sinking" the language design. For example, I think that promises in JS are a mess, and Go has it right with coroutines. The underlying problem was callback hell, and promises just replaced one set of problems with another and are, IMO, not nearly as easy to reason about.
"In BCPL,flexibility is retained by eliminating the concept of data types and representing all quantities by bit patterns of the same length. A single vector may, for example, hold bit patterns representing numbers, booleans and pointers to other vectors. Variables are declared before use, but simply as variables, not as variables of any particular type."
Sounds like a nightmare, this is basically the type system of vanilla verilog: Everything is just a bitvector of a certain length, each bit can have four states X,Z,0,1, so that it allows you to assign to a 32bit vector a 16bit vector resulting in the upper 16bit to be undefined. System verilog added structs, but if you declare them packed they allow unstructured assignment. Overall it is a complete nightmare given the fact that at the end you will tape out a chip that might cost >1 million to produce.
I assume you mean the static typing that Swift has, in favor of something like Python or Ruby, where the type of thing in a variable is both implicit and unfixed. Saying that there should be no types at all is...well, maybe it's just a failure of my imagination, but I don't even know how you could write a program. Even assembler has types.
Well I think basic data types are enough to express what the variable is expected to hold. Any highewr level types above that, such as what higher level languages provide are, I think a luxury for lazy programmers
Removing protective padding and helmets might help in American Football. Rugby doesn't use them and it's actually a safer sport.
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.
It's worth checking out Visual studio code. It is a fairly advanced JavaScript IDE in my experience. Using it with TypeScript is amazing if you're willing to put up with the overhead of compiling to JavaScript (though you can run TypeScript without a compile step in the server using node-ts). These tools make JavaScript a lot nicer for me, and actually pretty enjoyable!
Well the tooling you are referring to is more like buildingin an automatic sensor into a basic tool. So it's an enhancement to a basic rtool functionality, not a new tool. I'm only questioning the usefulness of that enhancement, since it results in programmers not caring about a certain aspect of their programming. It's sort of like exception handling, while it exists, it rarely gets activtaed in real programs.
I've read many people complain about static typing + more advanced type systems making programming too hard. This is the first time I've ever read someone assert that they make programming too easy.
It's a very well written article. I'm just a bit surprised there is no mention of the origins of this paradigm as it originated in functional languages (Maybe monad in Haskell).
Well, Either is a monad. Evolution of this style to avoid divergent nesting depths (The Codoken: http://i.imgur.com/BtjZedW.jpg) is a monad, even in the "railway" style of F#.
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-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.
You 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.
There 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.
It'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.
> the type of Handler3 can be had as Result<Tuple<Data, Address>, NSError>,
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 think "sum-type errors" are the wrong default. They force you to deal with the bureaucracy of different error types, which is useless because error handling almost never cares about error type. And they don't give you a stack trace, which is a lifesaver for development and debugging. 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 think "sum-type errors" are the wrong default. They force you to deal with the bureaucracy of different error types, which is useless because error handling almost never cares about error type.
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.
Yeah, agreed that exceptions make the exceptional path more expensive. But I think the normal path is cheaper.
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
Actually, I'm not entirely right here. Joe Groff caught up with me on twitter and explained to me that because Swift requires you to check exceptions, it actually generates static logic at the call site. This means handlers have a cost on both sides of the "railway", but it means that exception handling has a fairly optimal codepath.
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!
The reason I started using this pattern is one I didn’t see the article mention:
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.
To nitpick, the “both error and data” can happen, for example when the connection drops in the middle of a large response. Probably doesn’t change anything, but it’s good to know, maybe so that you don’t trap on an unexpected combination.
76 comments
[ 5.8 ms ] story [ 165 ms ] threadYou 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.
* 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.
One good thing about this is not having to push down anything to do with logging or reporting into subcomputations.
(Likely applicatives will take another decade to go mainstream.)
/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...
http://openwall.com/lists/oss-security/
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:
http://www.cl.cam.ac.uk/~mr10/mcplman.pdf
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!
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?
[0] https://developer.apple.com/documentation/swift/error
[1] https://boats.gitlab.io/failure/
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):
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!
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.
Or are they just two different problem domains?