Authors Top

We’re starting a new Scala area. If you have a few years of experience in the Scala ecosystem, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

1. Introduction

The name “Scala” was inspired by the words “scalable language”. Scalable here means that the language should grow with the demands and the needs of the users, making it possible to target a wide range of programming tasks. Technically speaking, Scala blends the object-oriented and functional programming paradigms in a statically-typed language.

In this tutorial, we’ll take a look at the main features of the Scala language, with some references to Scala 3. In particular, we’ll focus on the following ones:

  • concise syntax
  • expressive type system
  • support for both functional and object-oriented programming
  • seamless interaction with Java

2. High-Level Language and Concise Syntax

In Scala, programmers don’t have to deal with low-level concepts, such as memory management. Furthermore, as in every other functional language, we write what we want, not how to achieve it. The code we can produce in Scala is declarative rather than imperative. We could, in principle, write imperative code as well. Let’s start by writing an imperative function to sum the numbers in a Seq[Int]:

def sum(ns: Seq[Int]): Int = {
  var total = 0
  for (i <- ns) {
    total += i
  }
  total
}

Invoking the function above with sum(Seq(1, 2, 3, 4)) would give us 10 as a result. The exact same behavior can be achieved by leveraging Scala’s concise syntax:

def sum2(ns: Seq[Int]): Int = ns.foldLeft(0){ case (total, i) => total + i}

Furthermore, to instantiate an object, new is not required, making the code easier to read. Type inference allows us to omit the type of an expression on almost every occasion.

Finally, Scala 3 removed the need for curly brackets in the declaration of classes and traits:

trait Item:
  val price: Int

class DiscountedItem(override val price: Int, discount: Int) extends Item:
  def discountedPrice: Int = price - discount

We can then create a new instance of DiscountedItem and call the discountedPrice method by writing DiscountedItem(10, 4).discountedPrice. Having fewer parentheses makes the code much easier to read.

3. Expressive Type System

The goal of Scala’s type system has always been to allow programmers to write readable code while enforcing, at compile type, that abstractions and syntactic sugar were used in a safe manner. Scala 3 has introduced even more abstraction to simplify the programmer’s tasks.

Two very interesting additions are union types and extension methods

3.1. Union Types

We can use union types when we have to model alternatives of several other types without requiring those types to be part of a hierarchy. Let’s see an example:

def length(value: String | Int): Int = 
  value match {
    case s: String => s.length
    case n: Int => n.toString.length
  }

assert(length("Test") == 4)
assert(length(10) == 2)

In the example above, we defined a method named length accepting either an Int or a String. We then pattern-match on the parameter and return the number of characters, if the parameter was a String or the number of digits if it was a number. Without union types, it would have been not easy to achieve the same behavior, and the type of the value parameter would have been Any.

If we attempted to call length with a Long instead of a String or an Int, we’d get a compile error:

Found:    (10L : Long)
Required: String | Int
  length(10L)

3.2. Extension Methods

Extension methods let us add functionality to classes we don’t have control over. This includes closed classes that we cannot sub-class coming from third-party libraries. Let’s see an example and add a few methods to String:

extension(s: String) {
  def removeOccurrences(subString: String): String = s.replace(subString, "")
  def removeOccurrencesIgnoreCase(subString: String) =
    Pattern
      .compile(subString, Pattern.CASE_INSENSITIVE)
      .matcher(s)
      .replaceAll("")
}

In the example above, we defined two new methods on String, removeOccurrences, and removeOccurrencesIgnoreCase, to remove the occurrences of a substring in a larger String with or without taking into account case sensitivity. In Scala 3, we can invoke them on String quite simply:

assert("ThisIsJustATest".removeOccurrences("Just") == "ThisIsATest")
assert("ThisIsJustATest".removeOccurrencesIgnoreCase("just") == "ThisIsATest")

If we didn’t leverage Scala 3’s extension methods, we’d have to define those two methods in an object or rely on rich wrappers implemented using implicits. Both solutions are feasible but involve more boilerplate code than extension methods.

4. Support for Both Functional and Object-Oriented Programming

In Scala, we can write both object-oriented and functional code. Generally speaking, we use functional programming for the business logic, whereas we organize the code into classes for the sake of modularity. Scala functions are first-class values, meaning we can pass them as parameters for other functions.

The best practices recommend using immutability as much as possible. For example, the immutable collection classes will never mutate the original collection but will instead return a new instance. Mutable alternatives are possible as well.

Furthermore, every value, including functions, is an instance of a class. All types inherit from Any, with no visible distinction between primitive types and boxed types, as in Java.

5. Seamless Interaction with Java

Among the main features of the Scala language is its interoperability with Java. We can use Java libraries in our code, and we can use Scala libraries in Java code. In particular, using Java libraries in Scala applications is very common and useful when there is no native Scala alternative for a given task. Let’s see how we can use Java collections in Scala:

import java.util
import scala.jdk.CollectionConverters.*

@main
def main(): Unit = {
  val javaList = util.LinkedList[Int]()
  javaList.add(1)
  javaList.add(2)

  val scalaList: List[Int] = javaList.asScala.toList
  assert(scalaList == List(1, 2))
}

The example above creates a Java LinkedList and adds two elements, 1 and 2. Then, by using the converters in scala.jdk.CollectionConverters, we turn javaList into a Scala List. The latter evaluates to List(1, 2), as expected.

6. Conclusion

In this article, we saw some of the main features of the Scala language. We focused not only on the ability to write concise yet expressive code but also on the interoperability with Java.

As usual, you can find the code over on GitHub.

Authors Bottom

We’re starting a new Scala area. If you have a few years of experience in the Scala ecosystem, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

Comments are closed on this article!