The programs we write are complex beasts. Even the smallest changes can have ripple effects into unexpected parts of our apps. And of course since we’re programmers, we’ve worked towards programmatic solutions to this problem. To help us analyze the effects of changes to our code, we write a second program to check our first program. This second program makes assertions and checks expectations about the state of the program at any given point.
If you’re checking the state of the first program while its running, you’re talking about executing a test suite.
If you’re checking the state of the source code of the first program, you’re talking about static type checking.
A static type system is one where each object is given a type. The type has information about what messages objects of that type can respond to. Those messages, in turn, have slots in them for parameters. These slots are have the shape of other types, so that only objects of those types can fit in those slots. They let you set an expectation. “I am a method. I want these kinds of objects, which will in turn respond to these messages. When I’m done, I’m going to return this kind of object.”
Of course, you can circumvent this whole system by using prodigious casting or by using the id
type for everything. Using a so-called “untyped” system, like Ruby or the id subset in Objective-C, is purposefully crippling yourself. Instead of pretending that there are no types in such a system, we need to realize that we’re actually shoehorning every semantic type into one.
Types are useful, because the computer, dozens of times per minute, can assert that your code is meeting the expectations of the code around it. There’s a reason we call it type safety. You can also get this with a comprehensive test suite, but that means painstakingly writing hundreds of tests while you write code. The beauty of types when compared with tests is that, as long as you’re using semantic types to model your app’s universe, you’re going to get all of that type-checking for $free.99.
I’m not saying that tests aren’t tremendously valuable. Type-checking, generally speaking, can’t verify the behavior of your methods. But the rote tests in your system, the boring ones, the ones you hate writing, these should be automatically checked by the computer, with no input from you. To get even more benefit out of type checking, wrap even the simplest value objects (like NSString
) in a type: this pattern is called Tiny Types.
Tests and types reveal their similarities in other ways too. A Massive View Controller is completely untestable and doesn’t gain much from type checking either. A better architecture, which generally involves more objects (and more types!) with less logic each, will be both way more testable and way more type-checkable.
We write tests because programs never stay the same. Requirements change, new features are added, and old code must be updated to more closely match the domain. We use refactoring to make sure our domain and our code stay aligned. We don’t write tests to assure that our code works right now, we write them to assert our that our new code is isomorphic to our old code.
Types are useful in the exact same way. The moment you change a method’s signature, the compiler immediately points out where you were using the old methods’s name, and lets you fix it. As long as you’re refactoring in small composable chunks, you’ll immediately know what you’ve broken. Type checking isn’t a panacea: an app that compiles with no warnings isn’t a necessarily going to behave perfectly, but that’s true of a project with all tests passing as well.
Types and tests are very similar, both leveraging the power of computers to do lots of mechanical tasks quickly. If you’re not taking advantage of both tests and types to wrote stronger, safer code, you’ll never know what bugs you’re leaving in your wake.