Key things I picked up while working with Scala
By: Mayesha Kabir
Tags: Sofware Development, Functional Programming, Co Op, Scala

Technology vector created by fullvector

As an individual who, up until only a couple months back, predominantly worked with languages such as Java and C#, Scala seemed like a whole new world. In the beginning, it was quite challenging to grasp and get comfortable with as it was so different from what I was used to. Now, coming toward the end of my 4 month co-op placement here at Hootsuite, I’d like to share some of the things I learned whilst working with Scala.

No more Null values

NullPointerExceptions are thrown when a method unexpectedly returns null and when this possibility is unhandled in code. Code bases and patterns I was more familiar with would generally be scattered with many instances of these if item != null or try/catch statements as a safety check in order to avoid this and similar exceptions.

I soon came to realize how null is not really a necessity as it’s merely a representation of an optional value that can be absent. In Scala, the idea of returning null is strongly discouraged as we prefer an explicit type representation. Scala, therefore, provides the opportunity for a type safe, clean and error handled code base, where we use practices such as an Option , Either , or Try .

Option:

Option is a way to represent values that may or may not be available. Option[T] holds the optional value of type T which is either an instance of Some[T] , if there is a value present, or, an instance of the object None , if there is an absence of value.

def name(first: String, last: String): Option[String] = {

if (valid case) Some(first + last)

else None

}

Having the method signature return with an Option[T] , explicitly informs the caller if there is some value, they will get Some[T] for use or if there is a possible absence, a None is returned. We can then use pattern matching, Scala’s version of a switch statement, to handle these cases:

val greeting: String = maybeName match {

case Some(name) => “Hello ” + name //ex returns “Hello John Doe”

case None => “Hello No Name”

}

Either:

Either, similarly to Option, is an instance of two types: either a Left or a Right. By convention, Right is used on a successful return of our expected value, much like Some[T] is. Left is used as a replacement of None , a failure path in which we are presented with a missing value. Unlike Option, Either allows us to smoothly throw specific exceptions or other types of errors instead of merely returning None . This, thereby, provides the caller with more context when something goes awry.

def name(first: String, last: String): Option[Exception, String] = {

if (valid case) Right(first + last)

else Left(new NoNameException(“Invalid Name”))

}

Try:

Try, similar to both Option and Either, results in a successful computed value or some throwable in the case of error. Try[T] , if successful, is an instance of Success[T] and results in the value of type T . It is an instance of Failure[T] , however, if during computation something unexpected occurs.

def name(first: String, last: String): Try[String] = {

if (valid case) Success(first + last)

else Failure(new NoNameException(“Invalid Name”))

}

Having the return type to be Try[T] , informs the caller that the computation can result in an error and they need to deal with the possibility of it.

val greeting: String = maybeName match {

case Success(name) => “Hello ” + name

case Failure(exception) => “Hello No Name”

}

Benefits:

These changes result in both more explicit and refined code. Using Option , Either , and Try also led me to understand a new style of programming and, more importantly, a powerful and cleaner approach to handling errors.

Using Higher Order Functions

One thing quite unfamiliar when learning Scala code was trying to understand a more functional programming approach on things. Being used to objects, constructors, getters, setters, and so on, in Scala all these boilerplates and accustomed patterns were gone. What became very recurrent was the use of higher order functions, and as a result, I was deeply exposed to working with them.

A higher order function is simply one that can take other functions as parameters and can return other functions. This allows functions to be passed as arguments to other functions. Map is a key example of a highly used higher order function.

.map

Map is commonly used to perform an operation on each element of a collection, resulting in a new one. Say we are given a list of integers and we want to double the value of each element, we can use .map :

val myList = List(1, 2, 3)

val myNewList = myList.map(i => i * 2)

// List(2, 4, 6)

The compiler knows the parameter type based on the type .map expects, so no need to explicitly declare int i . Moreover, in Scala, an underscore is a placeholder for parameters inside anonymous functions, so a shorthand practice, we can also use an _ to write the same code:

val myList = List(1, 2, 3)

val myNewList = myList.map(_ * 2)

// List(2, 4, 6)

Other collections, like the List example used above, have their own map method. For example the Map Collection:

val myMap = Map(“a” -> 1, “b” -> 2, “c” -> 3)

val newMyMap = myMap.map{case (key, value) => key -> (value * 2)}

// Map(“a” -> 2, “b” -> 4, “c” -> 6)

Shorthand:

val myMap = Map(“a” -> 1, “b” -> 2, “c” -> 3)

val newMyMap = myMap.mapValues(i => i * 2)}

// Map(“a” -> 2, “b” -> 4, “c” -> 6)

Small Aside: Immutability and Referential Transparency

Immutable means that we cannot change or mutate variables. In Scala, we prefer immutable code.

Referentially transparency refers to if functions evaluate to the same result given the same argument values. This is an ideal practice and requires functions to be free of any outside sources that can modify it’s behaviour. To clarify what I mean with an example:

def timesTwo(x: Integer): Integer {

x * 2

}

Given timesTwo(3) , we know that passing in 6 in place of this method will provide the same output, however:

var two = 2

def timesTwo(x: Integer): Integer {

x * two

}

Because the variable two can be modified by external sources, this method is not referentially transparent as the call timesTwo(3) may not always result in 6 now.

Benefits of Map:

So what are the benefits of .map versus say a for loop? Well, this is a lot more polished and uses less lines of code than if we were to use a for loop for the exact same output. Maps also provide the immutability factor which for loops lack. Although for loops can be written, albeit with more code involved, to account for not modifying the original collection, maps provide this immutability cleanly. In addition, Map works inside its own scope and utilities the variables within. A for loop may need variables outside of its scope, further increasing the overall chance for unwarranted mistakes.

.flatMap

FlatMap, can be thought of as a combination of the .map function followed by .flatten for the final result. Flatten meshes a collection of collections to create one single collection with the elements now becoming the same type.

val myList = List(List(1,2), List(3))

val newMyList = myList.flatten

// List(1,2,3)

FlatMap is a lot more powerful than Map as it allows us to chain operations together and it also becomes extremely useful when we are working with optional values. For instance, say we have a list of strings and we want to convert them to a list of integers:

val strings = List(“1”, “2”, “a”, “b”, “3”)

using .map , we have Some and None in our list because toInt cannot convert strings “a” and “b” to integers:

val mapStrings = strings.map(toInt)

// List(Some(1), Some(2), None, None, Some(3))

using .flatMap , this can be avoided:

val flatMapStrings = strings.flatMap(toInt)

// List(1,2,3)

For Comprehensions

At first, when trying to comprehend Scala code, it was quite difficult for me to have a clear understanding of what these .map on top of numerous more .map functions were doing. For-comprehensions which, not to be confused with for loops, are syntactic sugars for the organization of multiple operations. Example, this line of code:

a.flatMap(x => b.map(y => function(x, y))

…is equivalent to the following piece, but using for-comps:

for {

x <- a

y <- b

} yield function(x, y)

The <- are calls to .flatMap and the yield calls .map . An = may used for any other basic variable assignments. Furthermore, see how it is much more clear to comprehend? One notable benefit of for-comprehensions are that they allow code to become a lot more understandable as it greatly improves readability when dealing with numerous operations.

Final Thoughts

Although I mentioned the difficulty of Scala numerous times, it is now by far one of the most rewarding languages I have learned and I hope this sheds some aid to anyone learning Scala for the first time! 🤗