Readable Scala Style

2021-03-02

The following is a somewhat opinionated style guide targeted at people primarily with Java background who want to benefit from using the mainstream (also known as Future-based) Scala stack and do so while optimizing their reward to effort ratio. This guide will be well suited for teams that need a reasonable set of simple guidelines to follow, especially at the beginning, until everyone develops good intuition for Scala.

Signal your intention with crystal clear names

When it comes to programming, it's better to be wrong than vague. This implies that you should document your intent in code as clear as possible. For example, if you have a choice between calling a variable id or documentId and you know that you're dealing with documents here, choose the latter. Generic names such as correlationId are OK in generic code, but using them in business logic is usually a bad idea. Likewise, avoid using data types as names, i.e it's better to use updated or created rather than timestamp or date.

Minimize the LOC measure by leaning towards longer lines

This is somewhat controversial, but when it comes to reading the code, I much prefer something that doesn't spread multiple screens. Of course, this doesn't mean that you should try to fit everything into a single line. Rather, try to group operations so that they make up a single logical action. For example, if you have a collection of elements and you need to get something from it through a sequence of method calls, feel free to keep them on the same line. If the level of abstraction throughout the method as consistent as it should be, every line can be viewed as a single finished thought.

Strive for methods that mostly consist of single line val assignments

If a method consists of multiple val assignments, it forms a very easy to follow structure. Provided that you named your values and methods reasonably well, your Scala code should read like English. Occasional branching is OK, but it shouldn't distract the reader from the main idea, and the main flow should be easily evident.

Keep nesting to a reasonable level

Scala offers great many tools to avoid excessive nesting. Rather than building a pyramid of flatMaps, we can always write a for-expression instead. Instead of writing multiple nested if-expressions, we can wrap everything in Try and then use a Failure with an application-specific exception to signal an erroneous condition. If we're not interested in distinguishing between different erroneous conditions, we, yet again, can use a for expression.

Always keep class members minimally exposed

By default, always prefer the private access and only resort to public (which is default in Scala) if you must. This will make it much easier for a reader to understand the code in order to change it. Fight the temptation to move functionality that might be useful somewhere else to the commons module where it will be available to every service: most of the time this move will be premature, require serious refactoring later and result in a lot of unnecessary changes rippling throughout the code base.

Do not keep mutable state in service classes

Most service classes are stateless collections of methods grouped together, because they perform related business functions and probably have similar or overlapping dependencies. Because they are stateless, they can safely be instantiated as @Singletons without any concern about concurrency. This is exactly the opposite of what classical object-oriented programming tells us to do, and this is perfectly fine as we're not doing OOP here.

Avoid putting logic in case classes

Whereas service classes should contain only methods but not state, case classes should contain state but not logic. Again, this is in direct contrast to what OOP tells us to do. Also, keep in mind that if the case class in question resides in the commons package, everything that was said about keeping exposure to a minimum is still relevant here.

Always destructure tuples

It's easy enough to create a tuple, but it's not always easy to read the code that is full of them. If you're calling zipWithIndex, for example, it's better to destructure the pair immediately thereby avoiding cryptic calls to ._1 and ._2. Likewise, never return a tuple from a method, especially public. If the need arises, use a properly named case class (possibly, defined within a service class to limit its scope).

Use for expressions for monadic sequencing only

In theory, for expressions can be used to express methods such as map, flatMap, filter, foreach, so in a way they behave like a mixture of the do notation from Haskell and list comprehensions from Python. In practice, filter and foreach are better left unsugared, especially when collections are involved. On the other hand, sequentializing monadic types like Future, Try, IO is a great way to make code more readable and reduce nesting.

Use val whenever possible and only resort to var if necessary

Unlike Haskell, Scala has the var keyword and allows developers to define mutable variables. However, in an expression-centric language they are almost never needed. For the most part, you should only consider using a method-local var in a very rare situation of performance optimization. Everything else is better done with vals.

Consider replacing while loops with @tailrec functions

Most iterations in Scala can be expressed in terms of library functions. However, sometimes there is a need to exit the iteration urgently and still return a result. This can be done either imperatively with while, var and breaks or with recursion. In many cases, recursion results in a more understandable code, so take time and try to implement it, but make sure that it can be annotated with @tailrec.

Prefer enumeratum to the standard Enumeration

The standard pattern for implementing enum in Scala is described on StackOverflow and often used by Scala beginners. This approach is very limited and in fact inferior to the standard Java enum in almost every way. Always prefer enumeratum as a way of implementing enums until Scala 3 is released, and only resort to using Enumeration to support legacy systems.

Consider using union enums with Either to signal expected errors

Java has the notion of checked exceptions, which is mostly viewed by the Java community as a design mistake. However, this concept allows developers to differentiate between expected and runtime errors. Even though all exceptions in Scala are essentially RuntimeException's, in many situations it is necessary to make expected errors part of the API. The best strategy here is to use Either with a custom error type as Left value and normal return value as Right. The custom error type should ideally be an algebraic data type, i.e should consist of non-intersecting concrete values. Passing Strings with an arbitrary error message in English as Left is almost always a bad idea.

Use Option only when the absence of value is expected

If properly used, Option completely eliminates the problem of NullPointerException in Scala code. Ideally, it should be used to signal to the caller that the value may or may not be present. Since Option is effectively part of the API, it forces the caller to make sure that both possibilities are covered. For example, findById methods of a low-level repository might return an Option to signal that the value might not be present in the database. However, if the entity is expected to exist for some other higher-level operation, its absence should be signalled with a failure.

Be mindful of where your Future is running

Most methods on Future require an ExecutionContext that specifies in which thread pool this particular piece of code is going to run. Often times, people simply define or import a global implicit value and forget about the problem altogether. Just as often this creates a situation when all code, including blocking and long-lasting operations, is running on a single CPU thread pool depriving other tasks from live threads.

Avoid accidental exception swallowing

Many monadic types such as Future or Try catch exceptions internally, and most of the time, this is exactly what you want. However, it's also very easy to accidentally swallow an exception completely thereby depriving the caller from ever knowing that an error took place. Always be careful with error propagation when using methods like recover and be doubly suspicious when you encounter a nested Future.

Keep inheritance use to an absolute minimum

Using inheritance to avoid code duplication is a terrible idea in Java, and it's no different in Scala. If there's some logic that can be expressed as a pure function and reused by other services from the same module, it's better to extract it as a helper method. Likewise, if the functionality of a certain class needs to be extended, use composition as GoF suggested in "Design Patterns" and Josh Bloch re-asserted in "Effective Java".

Avoid unnecessary method overloading

Method overloading in Scala works exactly the same way as in Java, but this doesn't mean that it should be used just as often. While Java has to distinguish between primitives and reference types, Scala doesn't have this problem, and overloading is usually a bad idea as it makes code less readable. And there are many other reasons to avoid overloading in Java and in Scala.