Hi, my name is Andrei and I am a Software Engineer at Zenitech. Previously, I worked for a year as Scala Developer added up to 3 years of experience on Java/Spring technologies.
The topic of this article revolves around Scala language, its differences between it and its older and more established “brother” Java and why it would be a good idea to try it, EITHER* for personal projects, or pure curiosity to become a more flexible developer and expand your problem-solving skills.
I will share a little story with you. When I applied for a position in my previous workplace, I was told I will be working on a Java project, but surprise hit me when I found out it would be a Scala one. At that moment, I did not know what it looked like and frankly, for the first couple of weeks, I hated it, impostor syndrome kicked in. Syntax was different, new type structures, new libraries, peer pressure and a team completely made of foreign people. After time went by, I started to grasp these concepts and liked them, even more than Java and currently have doubts if I would ever want go back to Spring. However, JVM knowledge is essential so you never get rid of all the performance problems related to that environment.
The scope of this article is to give you some insights about this beautiful yet tricky language, the advantages and disadvantages that made me fall in love with it. And I hope you will too. Community is small, especially in Romania, but popularity is rising and every knowledge shared is a step towards creating a bigger circle of developers with common interests who put passion in their work and have innovative ideas.
What is Scala anyway?
What makes Scala special?
In the following chapter, I will go through some of the language’s major features that sets Scala apart from its older “brother”. Firstly, take a look at this method:
We use the special word def to define a method. An object is similar to a class, but you specifically use it when you want a single instance of that class. List of parameters is defined as in Java, but the return type is provided at the end after “:”. Note that we can simply skip the return as the compiler knows what data type it is. Oh, and you don ‘t need “;” unless an exceptional situation like when you have to delimit statements and compiler helps you a lot. I know some of you folks are glad you got rid of that pesky semicolon.
1. Compact and readable
Scala is an incredibly powerful language with very compact and clean syntax in comparison to Java, which in turn increases productivity. Here’s a code snippet for comparison:
Looks nice and clean, doesn’t it? Scala does not require any getters or setters.
One of the main design goals of Scala is to run on a JVM and provide interoperability with Java. Scala is compiled to bytecodes, and you can use tools like “Java class file disassembler” to disassemble bytecodes generated by the Scala compiler. In most cases, Scala features are translated to Java and vice versa. Internally, Scala uses type erasure to be easily integrated with dynamically typed languages for the JVM. Some Scala features (such as Traits and static fields/methods/classes) don’t directly map to Java, and in those cases, you have to create workarounds. A whole article could be written on this delicate subject. Therefore, let’s focus on an easy example:
You can create Java objects, call their methods and inherit from Java classes transparently from Scala. Similarly, Java code references Scala classes and objects. In our case, the Scala class Student implements the Java interface Comparable<T> and works with files. The Java code uses a method from the companion object (find out later about them) Student, and accesses respective fields. It also uses JavaConverters to convert between Scala and Java collections.
3. Type inference
Most of the time, you don’t need to specify types of your variables. Instead, its powerful type inference will figure them out so you can enjoy the beauty of coding. Such a smart compiler 😊
In this interactive REPL window (Read-Eval-Print-Loop – playground for developers to prototype and test their methods), we define a class and two functions. Observe how the compiler infers the result types of the functions automatically, as well as all the intermediate values.
Notice: List [Student], List [String] without specifying the return type, it just knows.
Again, a large topic as well. But we will keep it short. Traits are reusable components that can be used to extend the behavior of classes. They are similar to Java 8 interfaces and contain both abstract and concrete methods along with properties -> more like a flexible way to combine interfaces with behaviors. Let’s see some examples:
Short advice: If the behavior will not be reused, then make it a concrete class. A sealed trait can be extended only in the same file as its declaration. If a class implements one trait, use keyword extends. If multiple inheritance, first use extends …, then with keyword. When we extend a trait, we need to provide the implementations of all the abstract members (both methods and properties). If we want to skip the implementation, we would have to make the inheriting class abstract. We can extend from multiple traits, but only one abstract class. Abstract classes can have constructor parameters, whereas traits cannot. It’s optional to override the concrete members of a trait. As of Scala 2.12, a trait gets compiled to a single interface class file. This is possible because Java 8 supports concrete methods in interfaces. There is, however, a major difference between a trait and an interface: when we have conflicting methods in the parent interfaces, the compiler expects us to resolve them using the super keyword.
5. Case classes and companion objects
A case class has all of the functionality of a regular class, and more. When the compiler sees the case keyword in front of a class, it generates code for us. A companion object in Scala is an object that’s declared in the same file as a class, and with the same name as that class. A companion object and its class can access each other’s private members. Both of them have many benefits:
- Case class constructor parameters are public val (immutable) fields by default;
- An apply method is created in the companion object of the class, like a Factory method, no need to use the new keyword to create a new instance of the class;
- An unapply method is generated, which lets you use case classes in match expressions;
- A copy method is generated in the class. Mostly used in functional programming paradigm, extremely helpful when you need to perform the process of cloning an object and/or updating fields during the cloning process;
- equals and hashCode methods are generated, which let you compare objects and easily use them as keys in maps.
The following code shows how to create multiple constructors with apply methods in a companion object. Just as adding an apply method in a companion object lets you construct new object instances, adding an unapply lets you de-construct instances. You rarely need to write an unapply method yourself. Instead, what happens is that you get apply and unapply methods for free when you create your classes as case classes rather than as the “regular” ones.
A default toString method is generated, really helpful in debugging. Astronomer and Doctor are defined as case classes that have unapply methods whose type signature conforms to a certain standard. Technically, the specific type of pattern matching shown above is known as a constructor pattern. The biggest advantage of case classes is that they support pattern matching (more on this later).
6. High-order functions
Higher order functions take other functions as parameters or return a function as a result. The terminology can get a bit confusing, we use the phrase “higher order function” for both methods and functions that take functions as parameters or that return a function. In a pure object-oriented world, a good practice is to avoid exposing methods parameterised with functions that might leak object’s internal state, thus violating encapsulation. There are a couple of ways to use these HOFs, for short. One of the most common examples is the higher-order function map which is available for collections in Scala. It applies an anonymous function on every element inside our collection.
Notice how factor is not declared as an Int in the above example. That’s because the compiler can infer the type based on the type of function map expects. Line 9: since the Scala compiler already knows the type of the parameters (a single Int), you just need to provide the right side of the function. Caveat: you need to use _ in place of a parameter name (it was factor in line 6).
It is also possible to pass methods as arguments to higher-order functions because the Scala compiler will coerce the method into a function.
Another reason to use higher-order functions is to reduce redundant code:
The new method, addPoints, takes the points plus a function of type Double => Double (function that takes a Double and returns a Double) and returns the new points after matches.
Finally, we can have functions that return functions:
7. Operator overloading ->
Scala is often called a pure object-oriented language: everything is treated as an object. Java-like primitive types and operators are completely absent. Additionally, since operators are methods, they can be changed and overloaded. A method with a single parameter can be used as an infix operator: 10 .+(1) or simply 10 + 1.
Below we have some examples of overloading:
This is an example of building an adjacency list that wants to know all the possible positions of a pawn on the board who moves on xOy axis, so we add our own + method operator
Here, we define <<= and >>= to determine a position of our point on a map, this time with consideration of a point position being greater or equal or smaller or equal than another.
8. By-name, by-value, closures
Everything shown above is done in call by-value format. In general, parameterised functions are evaluated similarly as operators. First, it evaluates all the function arguments, from left to right. Then it replaces the function application by the function’s right-hand side, and, simultaneously, it replaces the formal parameters of the function by the actual arguments. The call by-value strategy has the advantage that it evaluates every function argument only once. To make an argument called by-name, we simply prepend => symbol to its type. Call by-name evaluation is similar to call by-value, but it has the advantage that a function argument won’t be evaluated until the corresponding value is used inside the function body.
First method will evaluate 4 * 7 and pass 2 to the function’s body where x’s corresponding parameter is multiplied with itself to give 784. Second one ignores expression 4 * 7 because parameter y is not used and x again is multiplied with itself to give value 4.
Call by-value is often more efficient than call by-name because it avoids the repeated recomputation of argument expressions that call by-name entails. Additionally, it avoids other side effects because we know when the expressions will be evaluated.
9. Tail recursive
We can separate recursion problems into head and tail recursion. Head recursion, or classis recursion, carries the risk of a stack overflow error when the call stack gets awfully deep. A common pattern used to make a head recursive function into a tail-recursive function is to follow a series of simple steps:
- Keep the original function signature the same (i.e., sierpinski)
- Create a second function inside the original function, give it a new name and an “accumulator” input parameter. Don’t forget to add @tailrec annotation, otherwise compiler might notice you and compilation gives an optimisation warning
- Call the second function from inside the first function. When you do this, give the second function’s accumulator parameter “seed” values (i.e., 0 and a List of * to construct triangle).
You like fractals and chaos theory? Here is an implementation of the famous Sierpinski triangle in two ways: recursion using stack and tail recursion.
Now, there are some important differences: First method with the whirlpool sign is head recursive and could potentially create stack overflow error. Second one uses @tailrec annotation which needs to be added to the method and tells the compiler to verify if the code has been compiled with tail call optimization. Last call in sierpinski method must be the recursive one. If done correctly, then Scala can reduce the call stack down to one call.
10. Scala “famous” data structures and mechanisms
Scala has a couple of data structures and mechanism which cannot be found in Java. Let’s go through some of them:
a) Pattern matching
We encountered something in our case class snippet, called pattern matching. Keyword match and two or more possible branches. A successful match can also deconstruct a value into its constituent parts. Official documentation names it as switch on “steroids”. Let’s tackle this detail with yet another examples:
Function showNotification takes as a parameter the abstract type Notification and matches on the type of Notification (it figures out whether it’s an Email, SMS, or Whatsapp). In the case Email(sender, title, _) the fields sender and title are used in the return value but the body field is ignored with _. In the case Email(sender, _, _) if favoriteFootballTeams.contains(sender), the pattern is matched only if the sender is in the list of favorite football teams descriptions and calls showMyFavoriteTeamsNotification. Same goes to SMS.
Managing and representing optional values requires careful thought. Writing code that handles the absence of data is difficult and source of many runtime errors.
Scala’s Option is particularly useful because it offers type safety (we can parameterize our optional values) and also provides us with a set of functional capabilities that aim in having fewer bugs.
An Option[T] (with T as a generic type) can be either Some[T] or None object, which represents a missing value.
For instance, the get method of Scala’s Map produces Some(value) if a value corresponding to a given key has been found, or None if the given key is not defined in the Map:
Option type is used frequently in Scala programs and you can compare this with the null value available in Java which indicates no value.
If you read carefully the introduction, you would have seen an asterisk at Either. That’s because it is one of Scala’s special data structures which I personally like and has proven to be really useful in my day-to-day work. Either works just like Option, with a difference being that with Either you can return a String that describes the problem that occurred. Actually, what you do is wrap the problem inside a Left branch and expected value/computation in a Right branch. You can return anything you want inside of Left, generally the wanted intention, so as a practical matter you typically return a String or Throwable.
We know maths rules, right? Division by 0 is impossible, so we will wrap the correct computation in Right and return a custom error message when dividing by 0 in Left.
d) Try with Success and Failure
Concept introduced in Scala 2.10, very helpful in concurrency issues, comes in hand with Futures. Try type represents a computation that may either result in an exception, or return a successfully computed value. It’s similar to, but semantically different from the Either type. Instances of Try[T], are either an instance of scala.util.Success[T] or scala.util.Failure[T].
This example contains most of the concepts learned. If we have a method without parameters, we can call it without parentheses (line 23). In our case, Try can be used to perform division on a user-defined input (making use of scala.io.Stdin), without the need to do explicit exception-handling in all of the places where an exception might occur. An important property of Try shown in the above example is its ability to pipeline, chain and catch exceptions along the way. The flatMap and map combinators in the above example each essentially pass off either their successfully completed value, wrapped in the Success type for it to be further operated upon by the next combinator, or the exception wrapped in the Failure type usually to be simply passed on down the chain.
Keep in mind: for a variety of reasons, including removing null values from your Scala code, you want to use Option/Some/None pattern. Or, if you’re interested in a problem (exception) that occurred while processing code, you may want to return Try/Success/Failure from a method instead of Option/Some/None.
Some differences between Scala and Java
Table comparison Scala vs Java
|Nested codes and is less readable, despite being shorter||More readable despite too much boilerplate code|
|Lazy evaluation is supported, and hence it delays complex computation unless necessary||Lazy evaluation is not supported and does not support computation|
|Does not use static keyword||Uses a static keyword|
|Has a difficult learning curve||Is easier when compared with Scala|
|Statically typed language||Dynamically typed language|
|Machine-compiled language||Object-oriented language|
|It is possible to do data analysis in Scala using Apache Spark||Is not much used for data analysis, and it does not integrate with Apache Spark|
|Strong in the functional programming paradigm||Java 1.8 Streams started supporting functional programming|
|The actor model is used for concurrency||Thread based model is used for concurrency|
|Supports frameworks like Play, Lift, Akka||Java supports many frameworks like Spring, Grails, Akka and Play too|
|It supports multiple inheritances using classes||It supports multiple inheritances using interfaces|
|Documentation is not as good and detailed||Documentation is very good|
|The community is small, but gaining popularity||The community is huge and popularity still rising|
|The compiler is faster in Scala, because of tail call optimisation||The compiler is slow in Java and tail call optimisation can be simulated through special annotations processes|
|Hardware cost is less. Security updates are not frequent||Hardware cost is more. Since apps can be downloaded from any source, the software is not so safe|
|Has REPL (Read, Evaluate, Print, and Loop)||Does not have a REPL loop|
|Backward compatibility, especially when migrating from Scala 2 to Scala 3||Java is backward compatible. New version of the language can be run in an older version too|
|Scala’s validation API is faster and is more customised||Java’s validation API is complex|
|Scala has more structures such as case classes, type inference, and Java’s structures||Scala specific structures are not compatible in Java|
As we saw through all the showcased examples, Scala has many useful functions and strengths over Java, but in turn, Scala has a higher learning curve. If you are new to programming, I would advise against learning Scala as your first language. Start with Java -> beginner-friendly and easy to understand. Scala is more complex and less intuitive due to the functional paradigm and its features. Scala’s exotic syntax means a lot of aspects in the programming logic are implicit and ready to use, making the code shorter and clearer to read, but does not go under the hood.
Check your knowledge about the concepts presented here and many more here: https://www.scala-exercises.org/ Don’t forget to login with your Github account.
The joy of learning a language is felt the biggest when making projects. Put your problem-solving skills in motion. Here are some useful links I visit regularly:
- Baeldung -> tons of resources on Java, Spring Framework, Spring Security, but also Scala. In my opinion, one of the best places to learn JVM technologies for free on the internet;
- RockTheJvm -> Youtube channel with lots of tutorials and tips and tricks. The best part is the creator is Romanian and one of the biggest contributors to Scala community worldwide;
- Academy Lightbend -> company behind Akka framework used in distributed applications with high real-time data traffic. They also offer free and paid certificates to learn Scala and Akka. Play framework also falls under their umbrella;
If Scala is the language to start, at least have a good grasp of mathematical concepts. For experienced programmers who want to enter functional world, I have news: Scala excels on functional programming, I am biased here 100%, but give it a try a come back with feedback. I am convinced you will not be disappointed. Are you a security geek? Try Lift, believed to be the most secure framework out there.
If you have questions or any of the readers who are already working with Scala and would like to share their experiences or debate other subjects rather than programming, you can find me on Linkedin.