Posted on May 11, 2021
by Denis Redozubov
How We Choose Programming Languages at Typeable
Demystifying the way we choose technologies for development
I’ve been asked several times why I prefer using such programming languages as Haskell and Rust, since they are not the most widely used and popular tools. So, I’ve written this post to demystify what goes on in my head when I consider the selection of technologies.
Developing software that needs to meet the requirements for long-term operation and a defined reliability level is, in a sense, similar to a chess game. In both cases, it’s rather difficult for the human brain to comprehend full scenarios. Experience is of great importance, and every move/choice can be critical. Further comparison suggests that, just like in chess, development is very much positional, i.e. a whole set of moves can be deployed in preparation for a maneuver which results in winning a single pawn. It might appear that it’s merely about one pawn but, in a serious game, it may become a considerable advantage. Similar to a positional game on a chess board, the development and evolution of large-scale projects involve constant decision making focused on solving major tasks or implementing project requirements. The effect of all, even minor, solutions tends to accumulate by the endgame, or by the moment the software product is in operation. However, the difference complicating the situation is that, unlike in chess, software development cannot be solved by using a computer. You can’t find the best moves just by running a computer engine. That is why it is necessary to make many decisions incrementally, leading us to the goal, and all means of improving our position are worth using.
In a nutshell, solutions can be divided into several categories: architectural, procedural, and instrumental. Architectural solutions show the way we structure the project. Procedures define how we organize the work process and assure the implementation quality and correctness. Instruments, in turn, determine what the development team should use to achieve the goal. Today, end-to-end software development is carried out using a large number of tools: you need to formalize the requirements and the development process, write the software code and test it, assemble the release, etc. Despite this flow of tasks, selection of a programming language can be of the greatest importance because this choice determines the following set of parameters:
- Performance baseline.
- Peculiarities of software distribution and operation, for example, the interpreter’s requirements or static linking ability.
- Ecosystem of reusable libraries and components. I’d like to note that it is not only the number of libraries that matters, but also the quality of those relevant for you.
- Possibilities of parallel/concurrent/asynchronous operation of the programs, which may be important for many systems.
- The difficulty people face when learning the technology, which significantly influences both the language community and developer retraining.
- Language expressiveness which is somewhat subjective, but still being felt by developers. Additionally, the selection of programming language can have a strong influence on the structure of development. For instance, the language ecosystem tools may determine the way unit tests are written and the tests scope. A good infrastructure for property tests can give a boost to move in this direction, while the lack of good infrastructure for unit tests can make their development and support more challenging.
The tools also influence architecture-related issues – reuse of system modules is linked to how easy it is, from a conceptual viewpoint, to divide the units and structure the code. For instance, explicit work with the effect systems enables better code generalization and ensures that the software code unit doesn’t perform any input/output operations, such as network and disk operations. This makes it possible to consider safety and software architecture-related questions.
Considering this, you should be aware that the correct selection of a programming language for your project and team may have far-reaching implications. Keeping in mind the chess analogy, we remember that every minor advantage contributes a point in favor of the language, and can play a significant role in a large-scale development. It should also be noted that I’m talking about selecting development tools in situations which set no strict constraints on technology selection relating, for instance, to a large ecosystem already written in a certain language. At Typeable, we are guided by the following motives for general-purpose languages:
- The programming language should support static typing. This allows the developer to reduce the duration of each iteration of the code modification and its validation. This also results in a significant reduction in the number of bugs, both in terms of functional requirements and software security.
- Algebraic data types – it’s difficult to overestimate the influence of this feature when you start using it. This is a simple feature, absolutely necessary for invariant modeling. Sum types are also so essential that selecting a language where you need to simulate them using other constructs means creating obstacles at the outset.
- Flexibility of support and execution of multithreaded programs. Languages with GIL (Global Interpreter Lock) fail to meet this requirement from the very beginning. It’s desirable to be able to maximize hardware utilization and work with sufficiently high-level abstractions.
- A sufficient ecosystem of libraries. We also subjectively assess their quality. We don’t think it necessary to connect everything in the form of libraries, but such basic things as bindings to popular databases should be available.
- Clear minds in the community of developers who work with this language. A developer we would like to see in our team should be interested in CS and development. This in contrast to “easy-to-learn” technologies tempting people to get into IT for the sake of easy money, which greatly dilutes the workforce.
- We should have programming languages in our toolbox that allow creating software that meets strict time and memory requirements.
Considering all of the above, our toolbox should allow us to hold a steady position in any project coming our way. Going back to the chess analogy, these are the principles that let us play a highly positional game. A positional game one aimed at creating a long-term position that opens up possibilities for the player, minimizing the weaknesses. This as opposed to an attack-oriented game, i.e. “sharp game” associated with higher risks. The attacking player strives to end the game before the opponent is able to take up a strong defense. Sharp development includes programming contests, MVP for marketing experiments, several data science tasks and, in many cases, software development for Computer Science publications. They are similar in that they usually don’t require any long-term support, as they just have to work for a definite period of time. On the other hand, a positional game means long-term play where maintainability and updateability are the key characteristics. This is exactly what we do, and we need a solid foundation to be sure that the software we write and update can operate for a long time. Though such projects can also start as MVP, they are based on quite different assumptions.
So why do we select a technology based on precisely these considerations? Multiple reasons can be given. First of all, it’s a good idea to exclude the issues of technology fashion and trendiness to improve predictability over a large timespan. Even though a time-proven compiler with an active community is a conservative option, this choice is reliable in contrast to new fancy options that pop up each year. Surely, some of them will move from the last category to the first, but we will know this only later, probably after several years. Instead of the fashion trends, we strive to use fundamental Computer Science and the great number of research works devoted to this topic that have been applied to the programming languages we use. For example, type theory is a discipline close to both mathematics and CS, that deals with the fundamental issues of requirements formalization. This is exactly what we need to write software. Besides, this is the combined experience of other people engaged in “exact” sciences, and I believe it would be absurd to ignore this experience. It makes more sense to take such discipline as a basis rather than use nothing, or to use a subjective opinion based on the personal experience of a particular individual.
Secondly, we are looking for programming languages and compilers embracing the largest possible number of our principles. This is why, in addition to our favorite Haskell, we’ve added Rust into our toolbox. For real-time requirements and strict constraints on memory utilization, we need something rather low-level. The typing strictness in C is still far from perfect, so if we can use Rust for such tasks, we’d prefer to use it.
The third reason is that we create software primarily for our clients, and we’d like to protect them from our biases. That’s why we mustn’t exceed a certain risk level agreed with the client when we select a tool. But, even under these conditions, we use rather marginal technologies such as GHCJS, because the complex analysis of pros and cons still produce an attractive picture for us and our clients. We’ve already written about how we arrived at this decision: Elm vs. Reflex and Why Do We Need Transpilation into JavaScript?.
All means and theoretical justifications are worth considering when you work with large code bases and complex software; you need to somehow keep this complexity in check. Our idea of a correct approach is to protect every pawn, improve our position gradually and carefully, so that the project can exist in a good and stable state until the moment it will play a pivotal role in our clients’ businesses.