Scala 3 — Book

Why Scala 3?


There are many benefits to using Scala, and Scala 3 in particular. It’s hard to list every benefit of Scala, but a “Top Ten” list might look like this:

  1. Scala embraces a fusion of functional programming (FP) and object-oriented programming (OOP)
  2. Scala is statically typed, but often feels like a dynamically typed language
  3. Scala’s syntax is concise, but still readable; it’s often referred to as expressive
  4. Implicits in Scala 2 were a defining feature, and they have been improved and simplified in Scala 3
  5. Scala integrates seamlessly with Java, so you can create projects with mixed Scala and Java code, and Scala code easily uses the thousands of existing Java libraries
  6. Scala can be used on the server, and also in the browser with Scala.js
  7. The Scala standard library has dozens of pre-built, functional methods to save you time, and greatly reduce the need to write custom for loops and algorithms
  8. “Best practices” are built into Scala, which favors immutability, anonymous functions, higher-order functions, pattern matching, classes that cannot be extended by default, and more
  9. The Scala ecosystem offers the most modern FP libraries in the world
  10. Strong type system

1) FP/OOP fusion

More than any other language, Scala supports a fusion of the FP and OOP paradigms. As Martin Odersky has stated, the essence of Scala is a fusion of functional and object-oriented programming in a typed setting, with:

  • Functions for the logic, and
  • Objects for the modularity

Possibly some of the best examples of modularity are the classes in the standard library. For instance, a List is defined as a class—technically it’s an abstract class—and a new instance is created like this:

val x = List(1, 2, 3)

However, what appears to the programmer to be a simple List is actually built from a combination of several specialized types, including traits named Iterable, Seq, and LinearSeq. Those types are similarly composed of other small, modular units of code.

In addition to building a type like List from a series of modular traits, the List API also consists of dozens of other methods, many of which are higher-order functions:

val xs = List(1, 2, 3, 4, 5) + 1)         // List(2, 3, 4, 5, 6)
xs.filter(_ < 3)      // List(1, 2)
xs.find(_ > 3)        // Some(4)
xs.takeWhile(_ < 3)   // List(1, 2)

In those examples, the values in the list can’t be modified. The List class is immutable, so all of those methods return new values, as shown by the data in each comment.

2) A dynamic feel

Scala’s type inference often makes the language feel dynamically typed, even though it’s statically typed. This is true with variable declaration:

val a = 1
val b = "Hello, world"
val c = List(1,2,3,4,5)
val stuff = ("fish", 42, 1_234.5)

It’s also true when passing anonymous functions to higher-order functions:

list.filter(_ < 4) * 2)
list.filter(_ < 4)
    .map(_ * 2)

and when defining methods:

def add(a: Int, b: Int) = a + b

This is more true than ever in Scala 3, such as when using union types:

// union type parameter
def help(id: Username | Password) =
  val user = id match
    case Username(name) => lookupName(name)
    case Password(hash) => lookupPassword(hash)
  // more code here ...

// union type value
val b: Password | Username = if (true) name else password

3) Concise syntax

Scala is a low ceremony, “concise but still readable” language. For instance, variable declaration is concise:

val a = 1
val b = "Hello, world"
val c = List(1,2,3)

Creating types like traits, classes, and enumerations are concise:

trait Tail:
  def wagTail(): Unit
  def stopTail(): Unit

enum Topping:
  case Cheese, Pepperoni, Sausage, Mushrooms, Onions

class Dog extends Animal, Tail, Legs, RubberyNose

case class Person(
  firstName: String,
  lastName: String,
  age: Int

Higher-order functions are concise:

list.filter(_ < 4) * 2)

All of these expressions and many more are concise, and still very readable: what we call expressive.

4) Implicits, simplified

Implicits in Scala 2 were a major distinguishing design feature. They represented the fundamental way to abstract over context, with a unified paradigm that served a great variety of use cases, among them:

  • Implementing type classes
  • Establishing context
  • Dependency injection
  • Expressing capabilities

Since then, other languages have adopted similar concepts, all of which are variants of the core idea of term inference: Given a type, the compiler synthesizes a “canonical” term that has that type.

While implicits were a defining feature in Scala 2, their design has been greatly improved in Scala 3:

  • There’s a single way to define “given” values
  • There’s a single way to introduce implicit parameters and arguments
  • There’s a separate way to import givens that does not allow them to hide in a sea of normal imports
  • There’s a single way to define an implicit conversion, which is clearly marked as such, and does not require special syntax

Benefits of these changes include:

  • The new design avoids feature interactions and makes the language more consistent
  • It makes implicits easier to learn and harder to abuse
  • It greatly improves the clarity of the 95% of Scala programs that use implicits
  • It has the potential to enable term inference in a principled way that’s also accessible and friendly

These capabilities are described in detail in other sections, so see the Contextual Abstraction introduction, and the section on given and using clauses for more details.

5) Seamless Java integration

Scala/Java interaction is seamless in many ways. For instance:

  • You can use all of the thousands of Java libraries that are available in your Scala projects
  • A Scala String is essentially a Java String, with additional capabilities added to it
  • Scala seamlessly uses the date/time classes in the Java java.time._ package

You can also use Java collections classes in Scala, and to give them more functionality, Scala includes methods so you can transform them into Scala collections.

While almost every interaction is seamless, the “Interacting with Java” chapter demonstrates how to use some features together better, including how to use:

  • Java collections in Scala
  • Java Optional in Scala
  • Java interfaces in Scala
  • Scala collections in Java
  • Scala Option in Java
  • Scala traits in Java
  • Scala methods that throw exceptions in Java code
  • Scala varargs parameters in Java

See that chapter for more details on these features.

6) Client & server

Scala can be used on the server side with terrific frameworks:

  • The Play Framework lets you build highly scalable server-side applications and microservices
  • Akka Actors let you use the actor model to greatly simplify distributed and concurrent software applications

Scala can also be used in the browser with the Scala.js project, which is a type-safe replacement for JavaScript. The Scala.js ecosystem has dozens of libraries to let you use React, Angular, jQuery, and many other JavaScript and Scala libraries in the browser.

In addition to those tools, the Scala Native project “is an optimizing ahead-of-time compiler and lightweight managed runtime designed specifically for Scala.” It lets you build “systems” style binary executable applications with plain Scala code, and also lets you use lower-level primitives.

7) Standard library methods

You will rarely ever need to write a custom for loop again, because the dozens of pre-built functional methods in the Scala standard library will both save you time, and help make code more consistent across different applications.

The following examples show some of the built-in collections methods, and there are many in addition to these. While these all use the List class, the same methods work with other collections classes like Seq, Vector, LazyList, Set, Map, Array, and ArrayBuffer.

Here are some examples:

List.range(1, 3)                          // List(1, 2)
List.range(start = 1, end = 6, step = 2)  // List(1, 3, 5)
List.fill(3)("foo")                       // List(foo, foo, foo)
List.tabulate(3)(n => n * n)              // List(0, 1, 4)
List.tabulate(4)(n => n * n)              // List(0, 1, 4, 9)

val a = List(10, 20, 30, 40, 10)          // List(10, 20, 30, 40, 10)
a.distinct                                // List(10, 20, 30, 40)
a.drop(2)                                 // List(30, 40, 10)
a.dropRight(2)                            // List(10, 20, 30)
a.dropWhile(_ < 25)                       // List(30, 40, 10)
a.filter(_ < 25)                          // List(10, 20, 10)
a.filter(_ > 100)                         // List()
a.find(_ > 20)                            // Some(30)
a.head                                    // 10
a.headOption                              // Some(10)
a.init                                    // List(10, 20, 30, 40)
a.intersect(List(19,20,21))               // List(20)
a.last                                    // 10
a.lastOption                              // Some(10) * 2)                              // List(20, 40, 60, 80, 20)
a.slice(2, 4)                             // List(30, 40)
a.tail                                    // List(20, 30, 40, 10)
a.take(3)                                 // List(10, 20, 30)
a.takeRight(2)                            // List(40, 10)
a.takeWhile(_ < 30)                       // List(10, 20)
a.filter(_ < 30).map(_ * 10)              // List(100, 200, 100)

val fruits = List("apple", "pear")                 // List(APPLE, PEAR)
fruits.flatMap(_.toUpperCase)             // List(A, P, P, L, E, P, E, A, R)

val nums = List(10, 5, 8, 1, 7)
nums.sorted                               // List(1, 5, 7, 8, 10)
nums.sortWith(_ < _)                      // List(1, 5, 7, 8, 10)
nums.sortWith(_ > _)                      // List(10, 8, 7, 5, 1)

8) Built-in best practices

Scala idioms encourage best practices in many ways. For immutability, you’re encouraged to create immutable val declarations:

val a = 1                 // immutable variable

You’re also encouraged to use immutable collections classes like List and Map:

val b = List(1,2,3)       // List is immutable
val c = Map(1 -> "one")   // Map is immutable

Case classes are primarily intended for use in domain modeling, and their parameters are immutable:

case class Person(name: String)
val p = Person("Michael Scott")           // Michael Scott = "Joe"   // compiler error (reassignment to val name)

As shown in the previous section, Scala collections classes support higher-order functions, and you can pass methods (not shown) and anonymous functions into them:

a.dropWhile(_ < 25)
a.filter(_ < 25)
a.takeWhile(_ < 30)
a.filter(_ < 30).map(_ * 10)
nums.sortWith(_ < _)
nums.sortWith(_ > _)

match expressions let you use pattern matching, and they truly are expressions that return values:

val numAsString = i match {
  case 1 | 3 | 5 | 7 | 9 => "odd"
  case 2 | 4 | 6 | 8 | 10 => "even"
  case _ => "too big"
val numAsString = i match
  case 1 | 3 | 5 | 7 | 9 => "odd"
  case 2 | 4 | 6 | 8 | 10 => "even"
  case _ => "too big"

Because they can return values, they’re often used as the body of a method:

def isTruthy(a: Matchable) = a match {
  case 0 | "" => false
  case _ => true
def isTruthy(a: Matchable) = a match
  case 0 | "" => false
  case _ => true

9) Ecosystem libraries

Scala libraries for functional programming like Cats and Zio are leading-edge libraries in the FP community. All of the buzzwords like high-performance, type safe, concurrent, asynchronous, resource-safe, testable, functional, modular, binary-compatible, efficient, effects/effectful, and more, can be said about these libraries.

We could list hundreds of libraries here, but fortunately they’re all listed in another location: For those details, see the “Awesome Scala” list.

10) Strong type system

Scala has a strong type system, and it’s been improved even more in Scala 3. Scala 3’s goals were defined early on, and those related to the type system include:

  • Simplification
  • Eliminate inconsistencies
  • Safety
  • Ergonomics
  • Performance

Simplification comes about through dozens of changed and dropped features. For instance, the changes from the overloaded implicit keyword in Scala 2 to the terms given and using in Scala 3 make the language more clear, especially for beginning developers.

Eliminating inconsistencies is related to the dozens of dropped features, changed features, and added features in Scala 3. Some of the most important features in this category are:

  • Intersection types
  • Union types
  • Implicit function types
  • Dependent function types
  • Trait parameters
  • Generic tuples

Safety is related to several new and changed features:

  • Multiversal equality
  • Restricting implicit conversions
  • Null safety
  • Safe initialization

Good examples of ergonomics are enumerations and extension methods, which have been added to Scala 3 in a very readable manner:

// enumeration
enum Color:
  case Red, Green, Blue

// extension methods
extension (c: Circle)
  def circumference: Double = c.radius * math.Pi * 2
  def diameter: Double = c.radius * 2
  def area: Double = math.Pi * c.radius * c.radius

Performance relates to several areas. One of those is opaque types. In Scala 2 there were several attempts to create solutions to keep with the Domain-driven design (DDD) practice of giving values more meaningful types. These attempts included:

  • Type aliases
  • Value classes
  • Case classes

Unfortunately all of these approaches had weaknesses, as described in the Opaque Types SIP. Conversely, the goal of opaque types, as described in that SIP, is that “operations on these wrapper types must not create any extra overhead at runtime while still providing a type safe use at compile time.”

For more type system details, see the Reference documentation.

Other great features

Scala has many great features, and choosing a Top 10 list can be subjective. Several surveys have shown that different groups of developers love different features. Hopefully you’ll discover more great Scala features as you use the language.

Contributors to this page: