Static typing and how it helps to decrease production bugs
You can’t make perfect software. Nobody can. But you can minimize production issues or build a less error-prone system. This is how we use static typing to build more robust software.
Production environments were always scary and dangerous places. Development teams usually avoid it and, not so long ago, only experienced members had access to it to avoid breaking stuff there. It was also expensive to find bugs in this hideous place.
Gladly, we are already in a new software era. With the arrival of agile, development teams now use different approaches to deal with all different environments. DevOps teams build extensive CI/CD approaches that can deploy from commits to production within a few minutes while code is lint checked and tested during this run. Bug fixing or rollbacks are easily performed and a bug’s lifetime (pun intended) is decreasing as time goes by. Today, sometimes with several deployments every day, bugs are no longer difficult, expensive, or time-consuming to fix. There’s no reason to be afraid of production anymore.
Avoiding issues is still the motivation
Nevertheless, it doesn’t mean we should hang loose and not build robust software. Bugs are less expensive now but they still have their cost. One more developer fixing a bug is one less developer shipping features. Can you really spare one team member when the market demands more and more innovation? Of course not!
We move faster when we build strong, robust software. We also move faster when we deal with fewer bugs. This is why we try to avoid bugs at diconium. And it’s one reason why we adopt a “better to be safe than sorry” culture (avoid bugs instead of fixing them).
We move faster when we build strong, robust software. We also move faster when we deal with fewer bugs.
There are lots of ways to assure code quality. We should do code reviews frequently as an extra pair of eyes can help to spot bugs or bad approaches. Tools like SonarQube and linters improve quality by analyzing the code base and spotting code smells and vulnerabilities. CI/CD pipelines prevent developers to commit issues when integration and unit tests fail. Approaches like TDD (Test-Driven Development) or KISS (Keep It Simple Stupid) avoid higher code complexity to solve the same problems.
Ok, you got the point! We have a large number of possibilities to ensure quality code but I am here to tell you about how Static Typing is probably the most effective.
Note: Just because Static Typing drastically decrease bugs, it doesn’t mean we should stop using other approaches/methods/tools. The more code quality assurances, the better. But be aware that it also increases the development time. There’s always a trade-off between code quality and development time.
What is static typing?
Static typing is no more than a type system feature for a programming language. It means that every variable name is bound to a type. An attempt to bind the variable’s name to a different type would raise in an illegal statement. While it’s possible in dynamically typed languages to bind a variable name to an integer, then assign a string to it and, in the end, bind it to an object Car, in statically typed languages this would cause a type exception.
In Java as soon as you declare an int variable, you can’t change its value to a string.
// Java
int integerOnly = 1;
integer = “String”; // error: String cannot be converted to int
integerOnly = new Car(); // error: Car cannot be converted to int
While in Python, the same variable may be bound to several types during its lifetime: because it is a dynamically typed language.
# Python
intAndBeyond = 13
print(intAndBeyond) # 13 - intintAndBeyond = 'To the universe and Beyond'
print(intAndBeyond) # To the universe and beyond! - stringintAndBeyond = Car() # class Car
print(intAndBeyond.color)
Benefits and perks of type systems
The fact that variables are bound to only one type brings to the table several ways to avoid or detect bugs during the development cycle. Compilers, IDEs, or plugins can be used to trace type errors quickly enough. This is what we call Type Safety. Without a type system, it might take longer to spot the errors in the above Java example, for instance.
Type systems can also be used as documentation for the development team. Sure, it’s a good idea to create well-structured documentation on a separate tool alongside code but the development experience can always be improved for others on the codebase as types describe the ins and outs of a method, for instance:
// Kotlin
fun buyCar(model: CarModel, color: Color): Car {
/* implement function */
}
Whist dynamically typed languages would probably have to pollute the code with a few lines of comments:
// Javascript
/**
* @param {string} model - model to buy.
* @param {string} color - color of the new car
* @returns {car} - newly buyed car
*/
function buyCar(model, color) {
// implement function
return car;
}
This is why TypeScript was designed in the first place. It helps to catch mistakes early through a type system and to make JavaScript development more efficient and robust, obliterating one of the biggest JavaScript liabilities.
Besides that, things get more complicated when the codebase has to be refactored for dynamic typing since a lot of errors are not caught during development for those. On the other side, big issues from refactoring will end up as type errors for static typing.
Example: Imagine you have a method that returned a single car previously but after a few modifications, it returns a list of cars. Updating the declared type signature and fixing any compile errors will catch most, if not all places that need updating. Moreover, IDEs are a great aid when used with types as the autocomplete features seem to know about our code better than us and end up suggesting improvements or fixing undesirable bugs.
Last but not least, statically typed languages are usually more efficient as it leverage ahead-of-time compilation which happens before code execution. With the amount of information provided to the compiler in a statically typed language, it can freely compile to machine code without waiting for execution. On the other hand, dynamically typing uses just-in-time compilation which translates human code to machine code at the same time of execution because the types are not predetermined.
Types of types
Along with checks before code executions, typing can also avoid malfunctions. There are lots of types and each solves its problem.
For instance, in Java, Null Pointer Exceptions (NPE) are the most common exception and it’s found in more than 70% of Java’s production applications. It’s not by chance that Charles Antony Richard Hoare, the inventor of Null Reference, calls it his “billion-dollar mistake”. Gladly, Nullable types came to save the day for typed languages. On the other side of JVM, we have Kotlin which implements NPE checks. In Kotlin, the type system distinguishes between references that can hold null (nullable references) and those that can not (non-null references). For example, a regular variable of type String
can not hold null.
// Kotlin
var bigString: String = "this is really a big string"
bigString = null // Compiler says no! Compilation error
To allow nulls, it‘s possible to declare variables as nullable strings, by using a question mark ?
. But if you want to access the same property on smallString
, it would not be safe, and the compiler reports an error.
// Kotlin
var smallString: String? = "small str" // can be set null
smallString = null
val l = smallString.length // Compiler says no! variable can be null
To check for smallString
‘s methods, we would need to make sure that it was not null, therefore avoiding NPEs.
There are also Value Types that are not used in many programming languages. Value types have a single, public value parameter that is the underlying runtime representation. This type at compile time works like a wrapper but, at runtime, the representation is a Simple Type (types that are already built-in and come with the programming language like integers, strings, doubles, etc). By using this type, you get the type safety of a data type without the runtime allocation overhead. Let’s pretend we want to represent unique numbers for buyers and sellers and we want to make sure that we never swap them when they are stored or fetched on the database.
// Scala// BuyerId Value Type
class BuyerId(val value: String) extends AnyVal {}
// SellerId Value Type
class SellerId(val value: String) extends AnyVal {}val sellerId: SellerId = SellerId("1234")
val buyerId: BuyerId = BuyerId("4321")// method that accepts BuyerId and returns Buyer
getBuyer(buyerId) // compiler says ok// method that accepts SellerId and returns Seller
getSeller(buyerId) // compiler says no! type mismatch!
By creating different value types for buyers ids and sellers id, we make sure we can never switch the values by mistake despite being both String literals.
Naturally, there are a lot more types of types like Algebraic Data Types, Refined Types, Recursive Data Types, and so on but let’s keep with only the three above in this section because they solve the majority of the issues a programmer have.
Is it worth the trade-off?
Type systems are indeed a very good way of strengthening your application. We have seen above that:
- type safety improves bug detection and avoidance;
- types can be used as in-code documentation;
- IDEs and plugins for typed languages work as pain-killers on big refactors;
- Statically typed languages are usually more efficient due to ahead-of-time compilation.
But it also brings drawbacks. Statically typed languages usually have a lot more boilerplate code, on average the development lifecycle is slower because of complex programming and error handling, their start-up time can also be delayed and the learning curve for those languages are often bigger. In short, again, there will be a trade-off between quality and time but we believe that it’s worth the time to assure quality code and to decrease production bug event!
What is your approach?