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:
- Scala embraces a fusion of functional programming (FP) and object-oriented programming (OOP)
- Scala is statically typed, but often feels like a dynamically typed language
- Scala’s syntax is concise, but still readable; it’s often referred to as expressive
- Implicits in Scala 2 were a defining feature, and they have been improved and simplified in Scala 3
- 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
- Scala can be used on the server, and also in the browser with Scala.js
- The Scala standard library has dozens of pre-built, functional methods to save you time, and greatly reduce the need to write custom
forloops and algorithms
- “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
- The Scala ecosystem offers the most modern FP libraries in the world
- 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 an abstract class named
AbstractSeq, traits like
LinearSeq, and more.
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) xs.map(_ + 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.
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 assignment:
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) list.map(_ * 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 type assignment 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) list.map(_ * 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
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
Stringis 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 interfaces in Scala
- Scala collections 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 parallel and concurrent software applications
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
Because you’ll rarely ever need to write a custom
for loop again, 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
Here are some examples:
List.range(1, 3) // List(1, 2) List.range(1, 6, 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) a.map(_ * 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) val fruits = List("apple", "pear") fruits.map(_.toUpperCase) // 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 practices
Scala idioms encourage best practices in many ways.
For immutability, you’re encouraged to create immutable
val a = 1 // immutable variable
You’re also encouraged to use immutable collections classes like
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 functional programming, and their parameters are immutable:
case class Person(name: String) val p = Person("Michael Scott") p.name // Michael Scott p.name = "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"
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
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:
- Eliminate inconsistencies
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
using in Scala 3 make the language more clear, especially for beginning developers.
- 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
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.