Scala provides many different constructs so we can model the world around us:
- Classes
- Objects
- Companion objects
- Traits
- Abstract classes
- Enums Scala 3 only
- Case classes
- Case objects
This section briefly introduces each of these language features.
Classes
As with other languages, a class in Scala is a template for the creation of object instances. Here are some examples of classes:
class Person(var name: String, var vocation: String)
class Book(var title: String, var author: String, var year: Int)
class Movie(var name: String, var director: String, var year: Int)
These examples show that Scala has a very lightweight way to declare classes.
All the parameters of our example classes are defined as var
fields, which means they are mutable: you can read them, and also modify them.
If you want them to be immutable—read only—create them as val
fields instead, or use a case class.
Prior to Scala 3, you used the new
keyword to create a new instance of a class:
val p = new Person("Robert Allen Zimmerman", "Harmonica Player")
// ---
However, with universal apply methods this isn’t required in Scala 3: Scala 3 only
val p = Person("Robert Allen Zimmerman", "Harmonica Player")
Once you have an instance of a class such as p
, you can access its fields, which in this example are all constructor parameters:
p.name // "Robert Allen Zimmerman"
p.vocation // "Harmonica Player"
As mentioned, all of these parameters were created as var
fields, so you can also mutate them:
p.name = "Bob Dylan"
p.vocation = "Musician"
Fields and methods
Classes can also have methods and additional fields that are not part of constructors. They are defined in the body of the class. The body is initialized as part of the default constructor:
class Person(var firstName: String, var lastName: String) {
println("initialization begins")
val fullName = firstName + " " + lastName
// a class method
def printFullName: Unit =
// access the `fullName` field, which is created above
println(fullName)
printFullName
println("initialization ends")
}
class Person(var firstName: String, var lastName: String):
println("initialization begins")
val fullName = firstName + " " + lastName
// a class method
def printFullName: Unit =
// access the `fullName` field, which is created above
println(fullName)
printFullName
println("initialization ends")
The following REPL session shows how to create a new Person
instance with this class:
scala> val john = new Person("John", "Doe")
initialization begins
John Doe
initialization ends
val john: Person = Person@55d8f6bb
scala> john.printFullName
John Doe
scala> val john = Person("John", "Doe")
initialization begins
John Doe
initialization ends
val john: Person = Person@55d8f6bb
scala> john.printFullName
John Doe
Classes can also extend traits and abstract classes, which we cover in dedicated sections below.
Default parameter values
As a quick look at a few other features, class constructor parameters can also have default values:
class Socket(val timeout: Int = 5_000, val linger: Int = 5_000) {
override def toString = s"timeout: $timeout, linger: $linger"
}
class Socket(val timeout: Int = 5_000, val linger: Int = 5_000):
override def toString = s"timeout: $timeout, linger: $linger"
A great thing about this feature is that it lets consumers of your code create classes in a variety of different ways, as though the class had alternate constructors:
val s = new Socket() // timeout: 5000, linger: 5000
val s = new Socket(2_500) // timeout: 2500, linger: 5000
val s = new Socket(10_000, 10_000) // timeout: 10000, linger: 10000
val s = new Socket(timeout = 10_000) // timeout: 10000, linger: 5000
val s = new Socket(linger = 10_000) // timeout: 5000, linger: 10000
val s = Socket() // timeout: 5000, linger: 5000
val s = Socket(2_500) // timeout: 2500, linger: 5000
val s = Socket(10_000, 10_000) // timeout: 10000, linger: 10000
val s = Socket(timeout = 10_000) // timeout: 10000, linger: 5000
val s = Socket(linger = 10_000) // timeout: 5000, linger: 10000
When creating a new instance of a class, you can also use named parameters. This is particularly helpful when many of the parameters have the same type, as shown in this comparison:
// option 1
val s = new Socket(10_000, 10_000)
// option 2
val s = new Socket(
timeout = 10_000,
linger = 10_000
)
// option 1
val s = Socket(10_000, 10_000)
// option 2
val s = Socket(
timeout = 10_000,
linger = 10_000
)
Auxiliary constructors
You can define a class to have multiple constructors so consumers of your class can build it in different ways.
For example, let’s assume that you need to write some code to model students in a college admission system.
While analyzing the requirements you’ve seen that you need to be able to construct a Student
instance in three ways:
- With a name and government ID, for when they first start the admissions process
- With a name, government ID, and an additional application date, for when they submit their application
- With a name, government ID, and their student ID, for after they’ve been admitted
One way to handle this situation in an OOP style is with this code:
import java.time._
// [1] the primary constructor
class Student(
var name: String,
var govtId: String
) {
private var _applicationDate: Option[LocalDate] = None
private var _studentId: Int = 0
// [2] a constructor for when the student has completed
// their application
def this(
name: String,
govtId: String,
applicationDate: LocalDate
) = {
this(name, govtId)
_applicationDate = Some(applicationDate)
}
// [3] a constructor for when the student is approved
// and now has a student id
def this(
name: String,
govtId: String,
studentId: Int
) = {
this(name, govtId)
_studentId = studentId
}
}
import java.time.*
// [1] the primary constructor
class Student(
var name: String,
var govtId: String
):
private var _applicationDate: Option[LocalDate] = None
private var _studentId: Int = 0
// [2] a constructor for when the student has completed
// their application
def this(
name: String,
govtId: String,
applicationDate: LocalDate
) =
this(name, govtId)
_applicationDate = Some(applicationDate)
// [3] a constructor for when the student is approved
// and now has a student id
def this(
name: String,
govtId: String,
studentId: Int
) =
this(name, govtId)
_studentId = studentId
The class has three constructors, given by the numbered comments in the code:
- The primary constructor, given by the
name
andgovtId
in the class definition - An auxiliary constructor with the parameters
name
,govtId
, andapplicationDate
- Another auxiliary constructor with the parameters
name
,govtId
, andstudentId
Those constructors can be called like this:
val s1 = new Student("Mary", "123")
val s2 = new Student("Mary", "123", LocalDate.now)
val s3 = new Student("Mary", "123", 456)
val s1 = Student("Mary", "123")
val s2 = Student("Mary", "123", LocalDate.now)
val s3 = Student("Mary", "123", 456)
While this technique can be used, bear in mind that constructor parameters can also have default values, which make it seem that a class has multiple constructors.
This is shown in the previous Socket
example.
Objects
An object is a class that has exactly one instance.
It’s initialized lazily when its members are referenced, similar to a lazy val
.
Objects in Scala allow grouping methods and fields under one namespace, similar to how you use static
members on a class in Java, Javascript (ES6), or @staticmethod
in Python.
Declaring an object
is similar to declaring a class
.
Here’s an example of a “string utilities” object that contains a set of methods for working with strings:
object StringUtils {
def truncate(s: String, length: Int): String = s.take(length)
def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*")
def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty
}
object StringUtils:
def truncate(s: String, length: Int): String = s.take(length)
def containsWhitespace(s: String): Boolean = s.matches(".*\\s.*")
def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty
We can use the object as follows:
StringUtils.truncate("Chuck Bartowski", 5) // "Chuck"
Importing in Scala is very flexible, and allows us to import all members of an object:
import StringUtils._
truncate("Chuck Bartowski", 5) // "Chuck"
containsWhitespace("Sarah Walker") // true
isNullOrEmpty("John Casey") // false
import StringUtils.*
truncate("Chuck Bartowski", 5) // "Chuck"
containsWhitespace("Sarah Walker") // true
isNullOrEmpty("John Casey") // false
or just some members:
import StringUtils.{truncate, containsWhitespace}
truncate("Charles Carmichael", 7) // "Charles"
containsWhitespace("Captain Awesome") // true
isNullOrEmpty("Morgan Grimes") // Not found: isNullOrEmpty (error)
Objects can also contain fields, which are also accessed like static members:
object MathConstants {
val PI = 3.14159
val E = 2.71828
}
println(MathConstants.PI) // 3.14159
object MathConstants:
val PI = 3.14159
val E = 2.71828
println(MathConstants.PI) // 3.14159
Companion objects
An object
that has the same name as a class, and is declared in the same file as the class, is called a “companion object.”
Similarly, the corresponding class is called the object’s companion class.
A companion class or object can access the private members of its companion.
Companion objects are used for methods and values that are not specific to instances of the companion class.
For instance, in the following example the class Circle
has a member named area
which is specific to each instance, and its companion object has a method named calculateArea
that’s (a) not specific to an instance, and (b) is available to every instance:
import scala.math._
class Circle(val radius: Double) {
def area: Double = Circle.calculateArea(radius)
}
object Circle {
private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)
}
val circle1 = new Circle(5.0)
circle1.area
import scala.math.*
class Circle(val radius: Double):
def area: Double = Circle.calculateArea(radius)
object Circle:
private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0)
val circle1 = Circle(5.0)
circle1.area
In this example the area
method that’s available to each instance uses the calculateArea
method that’s defined in the companion object.
Once again, calculateArea
is similar to a static method in Java.
Also, because calculateArea
is private, it can’t be accessed by other code, but as shown, it can be seen by instances of the Circle
class.
Other uses
Companion objects can be used for several purposes:
- As shown, they can be used to group “static” methods under a namespace
- These methods can be public or private
- If
calculateArea
was public, it would be accessed asCircle.calculateArea
- They can contain
apply
methods, which—thanks to some syntactic sugar—work as factory methods to construct new instances - They can contain
unapply
methods, which are used to deconstruct objects, such as with pattern matching
Here’s a quick look at how apply
methods can be used as factory methods to create new objects:
class Person {
var name = ""
var age = 0
override def toString = s"$name is $age years old"
}
object Person {
// a one-arg factory method
def apply(name: String): Person = {
var p = new Person
p.name = name
p
}
// a two-arg factory method
def apply(name: String, age: Int): Person = {
var p = new Person
p.name = name
p.age = age
p
}
}
val joe = Person("Joe")
val fred = Person("Fred", 29)
//val joe: Person = Joe is 0 years old
//val fred: Person = Fred is 29 years old
The unapply
method isn’t covered here, but it’s covered in the Language Specification.
class Person:
var name = ""
var age = 0
override def toString = s"$name is $age years old"
object Person:
// a one-arg factory method
def apply(name: String): Person =
var p = new Person
p.name = name
p
// a two-arg factory method
def apply(name: String, age: Int): Person =
var p = new Person
p.name = name
p.age = age
p
end Person
val joe = Person("Joe")
val fred = Person("Fred", 29)
//val joe: Person = Joe is 0 years old
//val fred: Person = Fred is 29 years old
The unapply
method isn’t covered here, but it’s covered in the Reference documentation.
Traits
If you’re familiar with Java, a Scala trait is similar to an interface in Java 8+. Traits can contain:
- Abstract methods and fields
- Concrete methods and fields
In a basic use, a trait can be used as an interface, defining only abstract members that will be implemented by other classes:
trait Employee {
def id: Int
def firstName: String
def lastName: String
}
trait Employee:
def id: Int
def firstName: String
def lastName: String
However, traits can also contain concrete members.
For instance, the following trait defines two abstract members—numLegs
and walk()
—and also has a concrete implementation of a stop()
method:
trait HasLegs {
def numLegs: Int
def walk(): Unit
def stop() = println("Stopped walking")
}
trait HasLegs:
def numLegs: Int
def walk(): Unit
def stop() = println("Stopped walking")
Here’s another trait with an abstract member and two concrete implementations:
trait HasTail {
def tailColor: String
def wagTail() = println("Tail is wagging")
def stopTail() = println("Tail is stopped")
}
trait HasTail:
def tailColor: String
def wagTail() = println("Tail is wagging")
def stopTail() = println("Tail is stopped")
Notice how each trait only handles very specific attributes and behaviors: HasLegs
deals only with legs, and HasTail
deals only with tail-related functionality.
Traits let you build small modules like this.
Later in your code, classes can mix multiple traits to build larger components:
class IrishSetter(name: String) extends HasLegs with HasTail {
val numLegs = 4
val tailColor = "Red"
def walk() = println("I’m walking")
override def toString = s"$name is a Dog"
}
class IrishSetter(name: String) extends HasLegs, HasTail:
val numLegs = 4
val tailColor = "Red"
def walk() = println("I’m walking")
override def toString = s"$name is a Dog"
Notice that the IrishSetter
class implements the abstract members that are defined in HasLegs
and HasTail
.
Now you can create new IrishSetter
instances:
val d = new IrishSetter("Big Red") // "Big Red is a Dog"
val d = IrishSetter("Big Red") // "Big Red is a Dog"
This is just a taste of what you can accomplish with traits. For more details, see the remainder of these modeling lessons.
Abstract classes
When you want to write a class, but you know it will have abstract members, you can either create a trait or an abstract class. In most situations you’ll use traits, but historically there have been two situations where it’s better to use an abstract class than a trait:
- You want to create a base class that takes constructor arguments
- The code will be called from Java code
A base class that takes constructor arguments
Prior to Scala 3, when a base class needed to take constructor arguments, you’d declare it as an abstract class
:
abstract class Pet(name: String) {
def greeting: String
def age: Int
override def toString = s"My name is $name, I say $greeting, and I’m $age"
}
class Dog(name: String, var age: Int) extends Pet(name) {
val greeting = "Woof"
}
val d = new Dog("Fido", 1)
abstract class Pet(name: String):
def greeting: String
def age: Int
override def toString = s"My name is $name, I say $greeting, and I’m $age"
class Dog(name: String, var age: Int) extends Pet(name):
val greeting = "Woof"
val d = Dog("Fido", 1)
Trait Parameters Scala 3 only
However, with Scala 3, traits can now have parameters, so you can now use traits in the same situation:
trait Pet(name: String):
def greeting: String
def age: Int
override def toString = s"My name is $name, I say $greeting, and I’m $age"
class Dog(name: String, var age: Int) extends Pet(name):
val greeting = "Woof"
val d = Dog("Fido", 1)
Traits are more flexible to compose—you can mix in multiple traits, but only extend one class—and should be preferred to classes and abstract classes most of the time. The rule of thumb is to use classes whenever you want to create instances of a particular type, and traits when you want to decompose and reuse behaviour.
Enums Scala 3 only
An enumeration can be used to define a type that consists of a finite set of named values (in the section on FP modeling, we will see that enums are much more flexible than this). Basic enumerations are used to define sets of constants, like the months in a year, the days in a week, directions like north/south/east/west, and more.
As an example, these enumerations define sets of attributes related to pizzas:
enum CrustSize:
case Small, Medium, Large
enum CrustType:
case Thin, Thick, Regular
enum Topping:
case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions
To use them in other code, first import them, and then use them:
import CrustSize.*
val currentCrustSize = Small
Enum values can be compared using equals (==
), and also matched on:
// if/then
if currentCrustSize == Large then
println("You get a prize!")
// match
currentCrustSize match
case Small => println("small")
case Medium => println("medium")
case Large => println("large")
Additional Enum Features
Enumerations can also be parameterized:
enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
And they can also have members (like fields and methods):
enum Planet(mass: Double, radius: Double):
private final val G = 6.67300E-11
def surfaceGravity = G * mass / (radius * radius)
def surfaceWeight(otherMass: Double) =
otherMass * surfaceGravity
case Mercury extends Planet(3.303e+23, 2.4397e6)
case Earth extends Planet(5.976e+24, 6.37814e6)
// more planets here ...
Compatibility with Java Enums
If you want to use Scala-defined enums as Java enums, you can do so by extending the class java.lang.Enum
(which is imported by default) as follows:
enum Color extends Enum[Color] { case Red, Green, Blue }
The type parameter comes from the Java enum
definition, and should be the same as the type of the enum.
There’s no need to provide constructor arguments (as defined in the Java API docs) to java.lang.Enum
when extending it—the compiler generates them automatically.
After defining Color
like that, you can use it like you would a Java enum:
scala> Color.Red.compareTo(Color.Green)
val res0: Int = -1
The section on algebraic datatypes and the reference documentation cover enumerations in more detail.
Case classes
Case classes are used to model immutable data structures. Take the following example:
case class Person(name: String, relation: String)
Since we declare Person
as a case class, the fields name
and relation
are public and immutable by default.
We can create instances of case classes as follows:
val christina = Person("Christina", "niece")
Note that the fields can’t be mutated:
christina.name = "Fred" // error: reassignment to val
Since the fields of a case class are assumed to be immutable, the Scala compiler can generate many helpful methods for you:
- An
unapply
method is generated, which allows you to perform pattern matching on a case class (that is,case Person(n, r) => ...
). - A
copy
method is generated in the class, which is very useful to create modified copies of an instance. equals
andhashCode
methods using structural equality are generated, allowing you to use instances of case classes inMap
s.- A default
toString
method is generated, which is helpful for debugging.
These additional features are demonstrated in the below example:
// Case classes can be used as patterns
christina match {
case Person(n, r) => println("name is " + n)
}
// `equals` and `hashCode` methods generated for you
val hannah = Person("Hannah", "niece")
christina == hannah // false
// `toString` method
println(christina) // Person(Christina,niece)
// built-in `copy` method
case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
// result:
// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)
// Case classes can be used as patterns
christina match
case Person(n, r) => println("name is " + n)
// `equals` and `hashCode` methods generated for you
val hannah = Person("Hannah", "niece")
christina == hannah // false
// `toString` method
println(christina) // Person(Christina,niece)
// built-in `copy` method
case class BaseballTeam(name: String, lastWorldSeriesWin: Int)
val cubs1908 = BaseballTeam("Chicago Cubs", 1908)
val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016)
// result:
// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016)
Support for functional programming
As mentioned, case classes support functional programming (FP):
- In FP, you try to avoid mutating data structures.
It thus makes sense that constructor fields default to
val
. Since instances of case classes can’t be changed, they can easily be shared without fearing mutation or race conditions. - Instead of mutating an instance, you can use the
copy
method as a template to create a new (potentially changed) instance. This process can be referred to as “update as you copy.” - Having an
unapply
method auto-generated for you also lets case classes be used in advanced ways with pattern matching.
Case objects
Case objects are to objects what case classes are to classes: they provide a number of automatically-generated methods to make them more powerful.
They’re particularly useful whenever you need a singleton object that needs a little extra functionality, such as being used with pattern matching in match
expressions.
Case objects are useful when you need to pass immutable messages around. For instance, if you’re working on a music player project, you’ll create a set of commands or messages like this:
sealed trait Message
case class PlaySong(name: String) extends Message
case class IncreaseVolume(amount: Int) extends Message
case class DecreaseVolume(amount: Int) extends Message
case object StopPlaying extends Message
Then in other parts of your code, you can write methods like this, which use pattern matching to handle the incoming message (assuming the methods playSong
, changeVolume
, and stopPlayingSong
are defined somewhere else):
def handleMessages(message: Message): Unit = message match {
case PlaySong(name) => playSong(name)
case IncreaseVolume(amount) => changeVolume(amount)
case DecreaseVolume(amount) => changeVolume(-amount)
case StopPlaying => stopPlayingSong()
}
def handleMessages(message: Message): Unit = message match
case PlaySong(name) => playSong(name)
case IncreaseVolume(amount) => changeVolume(amount)
case DecreaseVolume(amount) => changeVolume(-amount)
case StopPlaying => stopPlayingSong()