Many times I have been asked why I prefer to use programming languages such as Haskell and Rust, because they are not the most widely used and popular tools. This post is written with the intention of demystifying what goes on in my head when I think about technology choices.
Software development with the requirements of long-term operation and a certain level of reliability is in a sense akin to a chess game. Variants of the development of events in both are quite difficult for the human brain to comprehend, the role of experience is of great importance and each move / choice can be of critical importance. The similarity continues in the sense that, like chess, the development is very “positional” – a whole series of moves can be aimed at preparing for a maneuver that will lead to a pawn win. It may seem that this is just one pawn, but for a serious game this can be a significant advantage. Like the positional game of the chessboard, during the design and development of large projects, we constantly make decisions that are aimed at solving larger problems or meeting the requirements of the project. The effect of every solution, even a small one, tends to accumulate towards the endgame, or by the time the software product is in operation. The difference that aggravates the situation is that software development, unlike chess, is not solved on a computer, and you cannot just take and find the best moves by running a computer engine. Therefore, it is necessary to make many decisions that incrementally lead us to this goal, and all means that improve our strategic “position” are good.
Solutions can be simplified into several categories: architectural, methodological and instrumental. Architects talk about how we structure a project. The methods determine how we organize the work process, make sure of the quality and correctness of the implementation. The tools define what the development team needs to work with in order to get the result. For a full development cycle, a large number of tools are used today: you need to formalize the requirements, the development process, you need to write program code and test it, build a release, etc. Despite such an abundance of tasks, the choice of a programming language can play the most important role, because it determines a set of the following parameters:
- Baseline on the speed of the software.
- Features of distribution and operation of the program, for example, the requirement of an interpreter or the possibility of static linking.
- An ecosystem of reusable libraries and components. I note that it is important not only the number of libraries, but also the quality of the ones that are relevant to you.
- Possibilities of parallel / concurrent / asynchronous program execution, which can be important for many systems.
- The difficulty of teaching people the chosen technology, which significantly affects both the language community and the repurposing of developers.
- Expressiveness of the language.
In addition, the choice of a programming language can also significantly affect methodological issues, for example, the tools of the language ecosystem can determine how and to what extent unit tests are written. A good infrastructure for property tests can give a boost in this direction, and the lack of a good infrastructure for unit tests can make them difficult to write and maintain.
The tools also affect architectural issues – the reuse of modules in the system is tied to how convenient it is to conceptually divide blocks and structure the code. For example, explicitly working with effects systems allows you to generalize code more and make sure that the code module does not perform I / O operations, such as working with network and disk, which allows you to reason about security and architecture.
Based on this, you should be aware that choosing the right programming language for your project and team can have far-reaching consequences. Bearing in mind the chess analogy, we keep in mind that every small plus will go to the piggy bank of the language and can play a significant role in a large development. It is also worth mentioning that we are talking about choosing a development tool for situations where there are no strict restrictions on the choice of technologies in connection, for example, with a large ecosystem already written on something. At Typeable, we are guided by the following considerations for general-purpose languages:
- Static typing must be supported by the programming language. This reduces the duration for each iteration of the code change and validation cycle for the developer. It also allows you to significantly reduce the number of bugs, both in terms of functional requirements and software security.
- Algebraic data types – it is very difficult to overestimate the impact of this feature, once you start using it. A very accessible and absolutely necessary thing when modeling invariants. The same types-sums are so indispensable that choosing a language in which they need to be modeled through other constructions is to set yourself barriers at the first step.
- Flexible capabilities to support and execute multithreaded programs. Languages with GIL (Global Interpreter Lock) do not immediately satisfy this requirement. I would like to be able to utilize the capabilities of iron well and have sufficiently high-level abstractions to work with.
- Sufficient ecosystem of libraries, subjectively assess their quality as well. We do not consider it necessary to connect everything in the form of libraries, but the most basic things, like bindings to popular databases, should be available.
- Bright heads in the community of developers in this programming language. The developer profile that we would like to see as our employee is a person interested in CS and development. In contrast to this, we can put the status of “easy to learn” technologies that attract people to IT for the sake of easy money, which greatly erodes the market for specialists.
- We must have programming languages at our disposal that allow us to implement software with strict requirements for processing time and memory consumption.
As a result of all of the above, there are enough blocks in our toolbox that allow us to take a confident position in many projects. Returning to the chess analogy, these are our principles that allow us to play a positional game. Positional play is a game aimed at creating a long-term position that opens up opportunities for the player and minimizes weaknesses. It is opposed to an attacking, “sharp” game, where there is a large share of risk, and the attacking player tries to complete this game before his opponent can take a good defensive position. “Sharp” development is Olympiad programming, MVP for marketing experiments, many tasks in data science, and often the creation of software that accompanies Computer Science publications. They are united by the fact that they often do not require long-term support, they only need to work out at a certain point in time. Positional play, on the other hand, is a long-term play where maintainability and upgradeability are key metrics. This is what we are doing, and we need a good foundation in order to be confident in the long-term performance of the software that we write and update. Such projects can also start with MVP, but they are done with completely different prerequisites.
Why is the list of considerations for choosing technologies exactly like this? There are several reasons for this. First, it is desirable to exclude the issues of fashion and trend of technology in order to increase predictability over a long period of time. A compiler with a long history and an active community is, albeit a conservative, but reliable choice, as opposed to the shiny new things that appear from year to year. Surely some of them will move from the last category to the first, but we will find out about this later, probably in years. Instead of trends, we are trying to use fundamental Computer Science and a large amount of research on this topic that has found application in the programming languages we use. For example: type theory is a related discipline of mathematics and CS that deals with the fundamental issues of formalizing requirements. This is exactly what we need to write software. In addition, this is the cumulative experience of other people engaged in the exact sciences, and it is somehow stupid, in my opinion, to neglect this experience. It is better to take such a discipline as a basis than not take anything, or take a subjective opinion based on the life experience of one single person.
Second, we are looking for the implementation of the largest number of principles we have taken in programming languages and compilers. For this reason, in addition to our beloved Haskell, Rust has begun to appear in our toolbox. With real-time requirements and tight limits on memory usage, we need something low-level enough. The typing stringency in C still leaves a lot to be desired, so if it is possible to use Rust for such a task, we would prefer to do it.
The third reason: we make software primarily for our customers, and we would like them to be protected from our prejudices. Therefore, when choosing an instrument, the risk cannot exceed a certain level, which must be agreed with the client. But even with such conditions, we have quite marginal technologies like GHCJS, because with the combined analysis of the strengths and weaknesses, the picture was still attractive for us and our clients. We already wrote about how we came to this decision: Elm vs Reflex.
When working with large code bases and complex software, all means and theoretical justifications are good, because you need to somehow be able to keep this complexity in check. Our idea of the right approach is to defend every pawn, improve its position smoothly and accurately, so that the project can survive in good condition until the moment when it can play a decisive role for our clients’ business. We wish you the same.