Readable code is better code
By: Niharika Gupta
Tags: Readable Code, Scala, Software Development, Programming

Introduction

Most beginner programmers face a lot of dilemmas while coding, as to what kind of code would be appreciated in the industry. Each company has their benchmarks and best coding practices and guidelines, however, there is one aspect which is agreed upon by everyone. That is Readability. A Readable code stays longer and is easier to maintain and understand. It also enables future developers to change the code easily. Scala beginners also face this difficulty, and I have tried to address a few common mistakes that are usually encountered.

Let’s take Movie class for all our examples:

object Genre extends Enumeration {

type Genre = Value

val HORROR, ACTION, COMEDY, THRILLER, ROMANCE = Value

}



class Movie(

movieName: String,

movieActors: List[String],

movieRating: Double,

movieGenre: Genre

) { val name: String = movieName

val actors: List[String] = movieActors

val rating: Double = movieRating

val genre: Genre = movieGenre

}

Do not use very big lambda

Using Lambda is quite helpful as it can be used without being assigned to a variable. However, complicated lambda functions could make code difficult to read and harder to debug. Let us take an example where we want to classify a movie into good bad or average.

var movies: List[Movie] =_



movies.map(movie => if (movie.rating < 4) "Bad" else if (movie.rating < 7) "Average" else "Good")

A better way is to let a separate function handle all the complex logic. This maintains the readability for the developer.

movies.map(classifyMovie)



def classifyMovie(movie: Movie): String = {

val rating = movie.rating

if (rating < 4) "Bad"

else if (rating < 7) "Average"

else "Good"

}

It is much more concise as well.

Prefer val over var

Using var often leads to accidental changes in variables. Immutable data structures are preferred compared to mutable ones in the functional programming world. Let us take an example where someone tries to change the actors in a movie.

val movie = new Movie(

"Mystic River",

List("Sean Penn", "Kevin Bacon"),

8,

Genre.THRILLER

)

movie.actors = movie.actors :+ "Laura Linney"

This operation is not allowed by the Scala compiler. In order to add two immutable lists, we need to create a new immutable list.

val updatedActors = movie.actors :+ "Laura Linney"

This is particularly helpful in a multithreaded system where two threads are trying to access the same variable. Using val will make it safe.

Pattern match instead of if-else

One of the best practices when coding in Scala is to use match expressions instead of traditional switch statements or unwieldy if/else statements. Let’s try to generate advertisements based on different movie genres.

def classifyGenre(genre: Genre): String = {

if (genre == HORROR)

"You should be scared!"

else if (genre == ACTION)

"Let's have a fight!"

else if (genre == COMEDY)

"You are so funny!"

else if (genre == THRILLER)

"Why so much suspense"

else if (genre == ROMANCE)

"A love story"

else

"I don't have a clue"

}

Now let’s try to use a pattern match for the same.

def classifyGenre(genre: Genre): String = {

movie.genre match {

case HORROR => "You should be scared!"

case ACTION => "Let's have a fight!"

case COMEDY => "You are so funny!"

case THRILLER => "Why so much suspense"

case ROMANCE => "A love story"

case _ => "I haven't a clue"

}

}

This piece of code is much clearer for such cases. It can be even used in place of complex if-else logic. Pattern matching can also be used with case classes to extract values, or other types as well.

Option, Some and None instead of null

In a function that can return null values we should use Option. Let us play the dumb charades game where we start by guessing the no of words in the movie name. Using simple if/else requires to perform an explicit null check every time.

def guessTheWords(movieName: String): Int = {

if (movieName != null) {

movieName.split(" ").size

} else

0

}

Here is another way to write the same function. Let’s make movieName as Option[String] instead of String in our original Movie class.

def guessTheWords(movieName: Option[String]): Int = {

movieName match {

case Some(x) => x.split(" ").size

case None => 0

}

}

Option is just like a container that can have zero or one element of a given type. In the above piece of code when the movie name is missing, Option is used to gracefully handle the NullPointerException. It is much more readable as well.

Handle enums when a value is not present

There are many ways to handle the case when the user inputs invalid enum value. One way is to return null in the function which is doing enum lookup.

def valueOf(genre: String): Genre = {

val lookup = Genre.values.find(_.toString == genre)

lookup match {

case Some(g) => g

case _ => null

}

}

A better way is to throw an exception which can help the user to correct the value with a meaningful message. Also, we should consider a case-insensitive comparison check.

def valueOf(genre: String): Genre = {

val genres = Genre.values

genres

.find(_.toString.toLowerCase == genre.toLowerCase)

.getOrElse(

throw new NoSuchElementException(

s"Supported values are ${genres} but found ${genre}"

)

)

}

Similar functionality can also be achieved using withName function. If the use-case demands early detection of errors at compile-time, we can consider using Sealed Traits but it has its own limitations.

Foreach vs Map for transformation

Let’s assume there is a method that performs translation of movie actor names from English to Spanish (could be any other language). The anonymous method is called translate. A simple foreach will invoke this function on all the elements in the collection movieActors and store the contents in a ListBuffer as List is immutable and cannot be modified.

def transformFunction(actors: List[String]): List[String] = {

var translatedActors = new ListBuffer[String]()

actors.foreach(translatedActors += translate(_))

translatedActors.toList

}

As we can observe, foreach is trying to modify a list that is external to it, known as the side effect, and is difficult to parallelize. Foreach is good for the use cases which involves a non-transformation operation on a collection. Let us use map for the above transformation.

def transformFunction(actors: List[String]): List[String] = {

actors.map(translate)

}

The second way is much more concise and it doesn’t require mutating a collection that exists outside the lambda expression. Map returns another list of the same size by applying a transformation to each member of the list. So, in performance it is definitely better than the foreach.

Case class instead of a tuple

Let’s say we want to recommend movie names based on customer ratings. Example: List((Mystic River,8.0), (The Lord of the Rings,8.9)). Scala tuple comes handy for such operations which act as a little container to access the individual elements.

def movieRatings(movies: List[Movie]): Unit = {

movies

.map(movie => (movie.name, movie.rating))

.filter(ratingTuple => ratingTuple._2 > 5)

.foreach(movie => print(movie._1, x._2))

}

Tuples are usually good for situations where we want to combine fewer things and don’t want to create a separate class for it. However, as the elements in tuple increases it becomes harder to understand the context. Let’s see how this becomes very straightforward with the help of the case class.

case class Rating(name: String, rating: Double)

def movieRatings(movies: List[Movie]): Unit = {

movies

.map(movie => Rating(movie.name, movie.rating))

.filter(_.rating > 5)

.foreach(movie => print(movie.name, movie.rating))

}

The addition of new fields is easier when we use case class in comparison to a tuple.

String interpolation vs string concatenation

Now, while playing our dumb charades game, let’s say someone guesses the first half of the movie name as The Lord of the Rings. The complete movie name can be constructed using string concatenation resulting in a new string using + operator. How compiler handles error in cases of string concatenation is not straightforward to understand.

def guessMovie(firstName: String, lastName: String): String = {

firstName + " " + lastName

}

When concatenating something to a String, we should consider string interpolation. It is more convenient, safe, coherent, and readable.

def guessMovie(firstName: String, lastName: String): String = {

s"$firstName $lastName"

}

The performance for string concat and interpolator (s, f, and raw)may vary according to the length of a string.

Conclusion

This is just the beginning. Code readability is a never-ending subject in the computer programming world. Some people might argue that code should be well commented for other developers to understand better. I believe that our small actions like following proper naming conventions of variables, maintaining line spacing, reducing logic complexity, and adhering to the above practices will definitely lead to more readable code.

References

https://github.com/lloydmeta/enumeratum/blob/master/enumeratum-core/src/main/scala/enumeratum/Enum.scala

https://stackoverflow.com/questions/33593525/scala-safe-way-of-converting-string-to-enumeration-value

https://stackoverflow.com/questions/28319064/java-8-best-way-to-transform-a-list-map-or-foreach

https://nrinaudo.github.io/scala-best-practices/tricky_behaviours/string_concatenation.html

https://medium.com/@dkomanov/scala-string-interpolation-performance-21dc85e83afd