The developer's thinking toolkit

The developer's thinking toolkit

About consistency, languages, and the way you think

The problem of code consistency

How would you ensure code consistency within a team? What is the proper coding style to choose? What is the right tool for the job? I'm sure everyone leading a team of developers asked himself these kinds of questions. We all know how to write good code, but the challenging part here is to keep it in a good shape for its entire lifetime and keep it maintainable. And while you can rely on toolings like linters and static analysis for adherence to rules and conventions, it is way harder to ensure that solutions to the problems would be coded in the same manner, and more importantly, understood by all developers in a team or across teams. Modern programming languages tend to be multi-paradigm and allow you to solve the same problem in various different ways.

Consider this snippet in C# :

int fibonacciLength = 10;
int firstNumber = 0, secondNumber = 1, nextNumber;

Console.Write(firstNumber + " " + secondNumber + " ");

for (int i = 2; i < fibonacciLength; ++i)
{
    nextNumber = firstNumber + secondNumber;
    Console.Write(nextNumber + " ");
    firstNumber = secondNumber;
    secondNumber = nextNumber;
}

That will print out the first 10 elements from the Fibonacci sequence:

0 1 1 2 3 5 8 13 21 34

Compare it to the following code that produces the equivalent output:

int fibonacciLength = 10;

Enumerable.Range(1, fibonacciLength)
    .Aggregate(new { Current = 0, Prev = 1 }, (acc, idx) =>
    {
        Console.Write($"{acc.Current} ");
        return new { Current = acc.Prev + acc.Current, Prev = acc.Current};
    });

While in both snippets the outcome is the same, the approach is quite different. Linters and static analysis tools would be happy with both snippets. But it is really bad for consistency to have a codebase with code written in diverse styles. In fact, the more diversity in style and approaches developers introduce the less readable and maintainable code is.

Team autonomy versus consistency

You might argue that not all teams are the same. Some teams are more mature than others and can choose their own way of doing things as long as they produce releasable software and manage changes. True to some extent. How about new colleagues who join your team? Will they share your conventions? Do they agree with the rules? Sure, you can raise the bar for entrance for outsiders who are unwilling to accept team rules, but will that help the company achieve its business goals? It would be unfair to require a C# developer to use a functional approach at all stages, just as it would be weird to ask a Scala developer to use OOP as the main approach. But the bigger the organization the more demand for consistency across engineering teams. The possible exception is when teams are stable and the turnover rate among developers is very low. But even that is not enough strong argument to establish uncommon to the language rules.

A good example

In most languages and ecosystems, there are style guidelines and code conventions. But some of them go further and imply a certain way of writing code. Golang is a good example. Go Style outlines not only a style guide but style decisions. It agrees on a set of principles for weighing alternate styles, helps minimize surprises in Go readability reviews, and helps readability mentors use consistent terminology and guidance. Three words used in that guide are:

  • Canonical: Establishes prescriptive and enduring rules

  • Normative: Intended to establish consistency

  • Idiomatic: Common and familiar

Typical Go code is boring. Readability and usability are key principles of the language. It improves programming productivity by enforcing you to write code in a certain way.

A bad example

C# is a good example of a feature-rich, fairly complex language that allows you to write code in a variety of styles and solve problems in a number of different ways. With all the guidelines and conventions you could follow, there is a high demand and pressure for consistency within and between teams in organizations using dotNET. With each new release of .NET and C#, new features are added to the language that doesn't always help achieve consistency and readability. It would be really nice to just follow Uncle Bob's advice to keep things simple. But what is simple? How to enforce simplicity? What tools to use for this?

Idiomaticity

This is a key to code consistency. Idiomatic means familiar or common. Writing code that is idiomatic for a particular programming language makes it way easier to follow and change. At the beginning of the article, the first code snippet is idiomatic - every C# developer will understand it and be confident to introduce changes to it. What's wrong with the second snippet? Actually nothing, it's just not common for C#. If you want to express it in a functional way use a different tool. In the .NET ecosystem, there is a beautiful F# language where you could express it in 3-4 lines of code:

let fibonacciLength = 10
let fibSeq = Seq.unfold (fun (a,b) -> Some(a+b, (b, a+b))) (0,1)
seq { 0; 1; yield! fibSeq |> Seq.take (fibonacciLength - 2) } 
|> Seq.iter (fun el -> printfn $"{el}")

And every F# developer will understand it. And that is idiomatic F# code. One last step to consistency is to make sure that everyone in a team follows the same style.

The right tool for the job

Imagine a carpenter doing his job only with a hammer in his arsenal. It is the same as if the software developer tries to solve all the problems with only one programming language and/or framework. Different problems require different approaches. So to be a professional it is important to have tools in your arsenal, to be a polyglot. Learn new programming languages, architecture patterns, and concepts. It does not only improve your skillset, and expand your mind, but will get you closer to your dream job and higher salary. Master good skills in at least 2 or 3 different programming languages. If you know C#, try F#. If you know F#, try Rust or Go. If you know JavaScript try TypeScript. Have you mastered TypeScript? Try Elm. In my opinion, functional-first programming languages have a big advantage over object-oriented or procedural languages - it is just a better toolbox for thinking and approaching day-to-day problems in the majority of cases. So try to change your defaults and see if that works for you, it is always easy to roll back.

Don't impose your style where inappropriate

That however doesn't mean that you should impose your preferred style where it doesn't fit. One example is the legacy codebase. It wouldn't be very productive to start introducing objects and OOP patterns in the Scala project or start extensively using LINQ everywhere in the C# project with the conventional use of for-while loops. This will introduce inconsistency and confusion among existing developers. Better to find out why it wasn't done in the first place: is it a good reason behind it or just a lack of awareness?

Use proper tools instead of imposing extrinsic style where it doesn't a good fit

Another not so obvious at first glance problem is when you try to introduce idiomatically correct code in one language into another language even within the same paradigm.

Too much idiomatic

As an example, let's say a developer with a Haskell background needs to implement a full deck of cards in F# which are both functional-first languages. Only Haskell with its Typeclasses has native support for applicative functors while in F# there is no native support for it. The possible code he writes could look like this:

type Suit = Diamonds | Hearts | Clubs | Spades

type Face =
    |  Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten
    | Jack | Queen | King | Ace

type Card = { Face: Face; Suit : Suit }

// Face list
let allFaces = [
    Two; Three; Four; Five; Six; Seven; Eight; Nine; Ten;
    Jack; Queen; King; Ace]

// Suit list
let allSuits = [Diamonds; Hearts; Clubs; Spades]

// Card list
let fullDeck = [fun f s -> { Face = f; Suit = s }] <*> allFaces <*> allSuits

That's how you would do it in Haskell. Do you see <*> notation? That is not something you can find in F# by default. A custom definition should be created for that operator to work as an applicative functor for a list:

let (<*>) fs l = fs |> List.collect (fun f -> l |> List.map f)

That will work just fine and generate a full deck of cards. But that's not idiomatic for F#. Haskell is a more advanced functional programming language and requires discipline and knowledge of more advanced concepts that are not necessarily needed to write good and functional F# code. It also introduces a problem here, because <*> is now a reserved infix operator in the global namespace and can't be used for other applicative functors.

The idiomatic way to generate a full deck of cards in F# would be like this:

// Card list
let fullDeck = [
    for suit in allSuits do
    for face in allFaces do
    yield { Face = face; Suit = suit } ]

It is a simpler and more practical approach that works for that language and will be understood without any additional effort.

Conclusion

The software development industry is young enough and not at the stage where other engineering industries are right now (e.g. chemical engineering, mechanical engineering, etc.) where they have established practices, approaches, and tools for problem-solving. The software landscape is extremely diverse and there are always will be new challenges to solve on the horizon. The more diverse your knowledge is the better you are prepared to face these challenges and find the most effective solution. On the other hand, working collaboratively, readable and maintainable code is a very important aspect of your day-to-day job. Here are some tips:

  • Use the right tool for the job

  • Write idiomatic code

  • Be a polyglot developer

  • Don't impose your style when it's not appropriate

  • Try functional languages for solving day-to-day problems