Scala supports both functional programming (FP) and object-oriented programming (OOP), as well as a fusion of the two paradigms. This section provides a quick overview of data modeling in OOP and FP.
OOP Domain Modeling
When writing code in an OOP style, your two main tools for data encapsulation are traits and classes.
Scala traits can be used as simple interfaces, but they can also contain abstract and concrete methods and fields, and they can have parameters, just like classes. They provide a great way for you to organize behaviors into small, modular units. Later, when you want to create concrete implementations of attributes and behaviors, classes and objects can extend traits, mixing in as many traits as needed to achieve the desired behavior.
As an example of how to use traits as interfaces, here are three traits that define well-organized and modular behaviors for animals like dogs and cats:
trait Speaker: def speak(): String // has no body, so it’s abstract trait TailWagger: def startTail(): Unit = println("tail is wagging") def stopTail(): Unit = println("tail is stopped") trait Runner: def startRunning(): Unit = println("I’m running") def stopRunning(): Unit = println("Stopped running")
Given those traits, here’s a
Dog class that extends all of those traits while providing a behavior for the abstract
class Dog(name: String) extends Speaker, TailWagger, Runner: def speak(): String = "Woof!"
Notice how the class extends the traits with the
Similarly, here’s a
Cat class that implements those same traits while also overriding two of the concrete methods it inherits:
class Cat(name: String) extends Speaker, TailWagger, Runner: def speak(): String = "Meow" override def startRunning(): Unit = println("Yeah ... I don’t run") override def stopRunning(): Unit = println("No need to stop")
These examples show how those classes are used:
val d = Dog("Rover") println(d.speak()) // prints "Woof!" val c = Cat("Morris") println(c.speak()) // "Meow" c.startRunning() // "Yeah ... I don’t run" c.stopRunning() // "No need to stop"
If that code makes sense—great, you’re comfortable with traits as interfaces. If not, don’t worry, they’re explained in more detail in the Data Modeling chapter.
Scala classes are used in OOP-style programming.
Here’s an example of a class that models a “person.” In OOP fields are typically mutable, so
lastName are both declared as
class Person(var firstName: String, var lastName: String): def printFullName() = println(s"$firstName $lastName") val p = Person("John", "Stephens") println(p.firstName) // "John" p.lastName = "Legend" p.printFullName() // "John Legend"
Notice that the class declaration creates a constructor:
// this code uses that constructor val p = Person("John", "Stephens")
Constructors and other class-related topics are covered in the Data Modeling chapter.
FP Domain Modeling
When writing code in an FP style, you’ll use these constructs:
- Enums to define ADTs
- Case classes
enum construct is a great way to model algebraic data types (ADTs) in Scala 3.
For instance, a pizza has three main attributes:
- Crust size
- Crust type
These are concisely modeled with enums:
enum CrustSize: case Small, Medium, Large enum CrustType: case Thin, Thick, Regular enum Topping: case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions
Once you have an enum you can use it in all of the ways you normally use a trait, class, or object:
import CrustSize.* val currentCrustSize = Small // enums in a `match` expression currentCrustSize match case Small => println("Small crust size") case Medium => println("Medium crust size") case Large => println("Large crust size") // enums in an `if` statement if currentCrustSize == Small then println("Small crust size")
Here’s another example of how to create and use an ADT with Scala:
enum Nat: case Zero case Succ(pred: Nat)
case class lets you model concepts with immutable data structures.
case class has all of the functionality of a
class, and also has additional features baked in that make them useful for functional programming.
When the compiler sees the
case keyword in front of a
class it has these effects and benefits:
- Case class constructor parameters are public
valfields by default, so the fields are immutable, and accessor methods are generated for each parameter.
unapplymethod is generated, which lets you use case classes in more ways in
copymethod is generated in the class. This provides a way to create updated copies of the object without changing the original object.
hashCodemethods are generated.
- A default
toStringmethod is generated, which is helpful for debugging.
You can manually add all of those methods to a class yourself, but since those features are so commonly used in functional programming, using a
case class is much more convenient.
This code demonstrates several
case class features:
// define a case class case class Person( name: String, vocation: String ) // create an instance of the case class val p = Person("Reginald Kenneth Dwight", "Singer") // a good default toString method p // Person = Person(Reginald Kenneth Dwight,Singer) // can access its fields, which are immutable p.name // "Reginald Kenneth Dwight" p.name = "Joe" // error: can’t reassign a val field // when you need to make a change, use the `copy` method // to “update as you copy” val p2 = p.copy(name = "Elton John") p2 // Person = Person(Elton John,Singer)
See the Data Modeling sections for many more details on