Scala, Java’s younger “brother” – Top 10 features

Introduction

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?

Scala is a statically typed programming language who supports both object-oriented programming and functional programming. It started as a project in Switzerland, at École Polytechnique Fédérale de Lausanne, by Martin Odersky and released in 2003. Currently, there are two main versions: Scala 2.13 and Scala 3, the latter having several syntax differences and not quite adopted in the industry on a large scale. In this article, we will focus only version 2.13. Scala can run on JVM, on Javascript in your browser through Scala.js support and LLVM compiler, which is in beta stage the moment this article was released. At enterprise level, you can build high performant applications and distributed systems which scale upon clients’ demands.

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:

Scala exemple

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. 

Let’s start:

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:

Scala class example

Looks nice and clean, doesn’t it? Scala does not require any getters or setters. 

2. Interoperability

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:

Scala Interoperability
Scala Interoperability 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.

Scala type interference example

Notice: List [Student], List [String] without specifying the return type, it just knows.

4. Traits

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:

Scala traits example

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.

Scala classes and objects example
Scala classes and objects example

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.

Scala high order functions

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. 

Scala high order functions

Another reason to use higher-order functions is to reduce redundant code:

Scala high order functions

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:

Scala high order 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:

Scala overloading operator example

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

Scala overloading operator example

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.

Scala by name, value and closures

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). 
Scala recursive tails

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:

Scala pattern matching

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. 

b) Option

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:

Scala option pattern example

Option type is used frequently in Scala programs and you can compare this with the null value available in Java which indicates no value.

c) Either 

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.

Scala either structure example

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]. 

Scala try with success and failure

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.  

Bonus:

Some differences between Scala and Java 

Table comparison Scala vs Java

ScalaJava
Nested codes and is less readable, despite being shorterMore readable despite too much boilerplate code
Lazy evaluation is supported, and hence it delays complex computation unless necessaryLazy evaluation is not supported and does not support computation
Does not use static keywordUses a static keyword
Has a difficult learning curveIs easier when compared with Scala
Statically typed languageDynamically typed language
Machine-compiled languageObject-oriented language
It is possible to do data analysis in Scala using Apache SparkIs not much used for data analysis, and it does not integrate with Apache Spark
Strong in the functional programming paradigmJava 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, AkkaJava supports many frameworks like Spring, Grails, Akka and Play too
It supports multiple inheritances using classesIt supports multiple inheritances using interfaces
Documentation is not as good and detailedDocumentation is very good
The community is small, but gaining popularityThe community is huge and popularity still rising
The compiler is faster in Scala, because of tail call optimisationThe compiler is slow in Java and tail call optimisation can be simulated through special annotations processes
Hardware cost is less. Security updates are not frequentHardware 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 3Java 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 customisedJava’s validation API is complex
Scala has more structures such as case classes, type inference, and Java’s structuresScala specific structures are not compatible in Java

Conclusion

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:

  1. 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;
  2. 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;
  3. 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.

Author avatar
Andrei Muja

Adaugă un comentariu:

Adresa ta de email nu va fi publicată. Câmpurile obligatorii sunt marcate cu *