Making an effort to unravel the complicated, and simplify the complex
Writing apps can be hard. Writing apps that are free from bugs is nigh on impossible. Explicit types can help, but unless you are talking about using an exotic language like Agda, or other Dependently Typed languages, then having types is not enough, by itself, to keep our systems safe from exceptions at runtime. Some effort has to go into the programmer’s work, to help the compiler out, because there are going to be some blind spots. With that said, not all independently-typed languages are equal; a Haskell or a PureScript or an Elm is going to do a much better job at preventing runtime errors than, say, C, due to the checks and the tools available to the compiler and the developer.
Where is the power coming from, in these more exotic lands? How can we take advantage of some of the same concepts in some of the more common languages in the day-to-day of working for companies that are a little less bleeding-edge?
One of the reasons that the Elm authors and adopters claim that it runs without runtime errors, and “if it compiles, it probably works”, is due to living in a Closed System.
But what does that even mean? — S.Traumann
Great timing, tired narrative helper! Glad you asked.
This is a Finite State Machine which describes the constraints and the behaviours of a coin-operated turnstile. If you squint, you can also apply that machine to gumball machines, or cheap vending machines, or really a lot of things, by scribbling out some words and replacing them.
This is (hopefully) a Closed System. We know all of the behaviours. We know all of the dependencies. We know the shape, size and value of the coin or token that the machine is willing to accept. We can always figure out what state we should be in, and where we should go, based on where we are and what we have. It’s Zork for amusement parks and transit systems.
If we add more potential states, it gets more complex to work with, but it can still be a closed system as long as you can clearly define all valid transitions between all states. I’m going to avoid talking about structuring applications via FSM, for now. Instead, I’m going to argue that the part that we tend to have the most problems with in our applications is the coin.
Rather than a turnstile, allow me to try to illustrate with an app that ought to function like a gumball machine: put a token in, and based on the value of the token, you get the equivalent in candy.
There’s not a whole lot going on. If you give me a token worth 2, I give you 2 candies. If you give me a token worth 5, I give you 5 candies. The compiler is happy, the QAs are happy with the desk checks… all that is left is to hook it up to the online token service, and it’s ready to go out into the wild.
Well… first, there are a lot of complaints that the system has eaten the user’s money, without delivering anything. Those have been followed by reports that people might be gaming the system, and running off with candy that doesn’t fit neatly into the amounts the system is designed to work with.
How could either of those things possibly happen? The compiler said everything was great!
…but the compiler isn’t running at runtime, and didn’t take the type-safety of the entire internet (ie: none) into account, when it was compiling.
You do a little digging into
processTransaction; turns out, if anything goes wrong when handling the payment, the token ends up being returned as
null. It also appears that it’s possible to get a token worth 7, based on some outdated requirements, and a token value of 10.2, due to a weird gap in the logic with the exchange rate.
We know how to fix all of those.
No more exceptions! We handle the null case, we check that the token value is an integer… and if we can’t make it work the way it’s supposed to, we just return
undefined and made a quick change to the return type to support having no value. Done! Wrap it up and go home.
…but did we really solve the problem, though? We solved the code that was crashing in our service. We solved cases where people might be cheating the system. But did we really solve the problem of users’ money disappearing with no candy in return?
Nope. Not even a little. In fact, the people who were buying 7 pieces earlier would have been receiving 7 pieces from our service. Those people are still paying money, but all they are being delivered is
What can we do about it, then? If we can’t go overboard checking that the token is valid inside of the service, how do we make sure it’s working correctly?
Well, that’s just it. The service is a closed system; or it used to be. In order to do its job perfectly, it expected to receive a valid token, and to return a valid set of candy. As soon as you send a dubious token through, the whole codebase is at risk of being clogged with checks and casting that will only add confusion, and returning values which will only serve to break the next person in line, with no context of the problem you had. If that starts getting hairy inside of 8 lines of code, think about how scary a 140,000 line long app might be. In order to close that system back up, you need to identify all possible paths, to transition from one state to the other. In other words, you need to know all possible permutations of all possible loops and branches, so you can ensure that all possible exits from your state are known, and valid.
The easiest way to ensure that is to ensure that only valid data enters your system.
Let me say that one more time:
The easiest way to ensure that you are working in a correct system, or even have the ability to ensure that it can ever be correct, is to ensure that only valid data ever enters your system, and all invalid input is rejected before you do anything else.
Those few lines do a better job of fixing your app’s behaviour than all the if/else checks you could possibly care to add, on the inside. Checking here, instead, leaves you better informed about what to do in the case the token is missing, malformed, or otherwise ill-suited for the job.
If you’ve ever played a quarter-fed arcade machine, take note that they don’t check the quarter’s validity while you are pushing the jump button, or moving the joystick (regardless of how much it might feel that way); they check it before they ever let you hit the start button. And if it is invalid in a way that is recoverable, they will throw your bad input out the coin-return for you to try again. Bubbles in a fluid line. Gaps in a seal. These are things which need to be checked and prevented at the boundary, because letting bad input in can wreak major havoc once inside the system, whether we’re talking about bloodstreams or brake lines, concrete or space suits.
Why is code different?
It’s not; that question was rhetorical. Next time, I’m going to take a swing at outlining some of the biggest contributors to bad input at boundaries, and try to offer safe and efficient methods for dealing with it.