Scala OOP Galore

Published on February 15, 2018 by Vasily Vasinov

Scala is a hybrid programming language that implements functional and object-oriented paradigms. With Scala, there is always more than one way to do something and oftentimes it can feel overwhelming. It could be confusing to see so many different OOP constructs ranging from trivial classes to traits, objects, and case classes. In this lesson we’ll go over major Scala OOP structures and learn how they are used in real life without forced mammal or fish tank analogies.

Classes

Scala uses class-based inheritance, so it’s no surprise that basic classes are at the core of its OOP model. Classes work great for organizing code.

Engineers that switch to Scala tend to adopt functional paradigms. Combining those with classes could be disorienting at first. It’s important to remember that object-oriented abstractions are just a subset of a larger set of functional concepts rooted in lambda calculus. It’s possible to use classes for code organization without breaking referential transparency and introducing global mutable state.

Referential Transparency

Referential transparency is a property of expressions that can be replaced with their corresponding values without changing the program’s behavior.

An example of a referential transparent expression is a pure function. Pure functions’ return values are determined by their input values without observable side effects. In other words, pure functions always return the same output for the same set of inputs.

Consider the following basic example of a Scala class written in imperative—or stateful—style:

class Logger(var context: String = "generic") {
  def info(m: String) = s"INFO: $context: $m"
  def error(m: String) = s"ERROR: $context: $m"
}

// Scala automatically generates getters and setters
// for `context`.

val l = new Logger()

l.info("test message")
// returns `INFO: generic: test message`

l.context = "controller"

l.info("test message")
// returns `INFO: controller: test message`

Here we implemented a class with a custom constructor that sets context to some custom value. Since functions info() and error() use context they automatically become referentially opaque. As a result they return different values depending on the mutable state of context. This variable can be changed from the inside and outside of the Logger object.

How can we fix it? There are at least two options. We could use a constructor with private instance variables, so nobody from the outside can change them after a new class instance is created:

class Logger(private var context: String = "generic") { 
  def info(m: String) = s"INFO: $context: $m"
  def error(m: String) = s"ERROR: $context: $m"
}

val l = new Logger("controller")

l.info("test message 1")
// returns `INFO: controller: test message 1`

// the following line will generate an error:
// `variable context in class Logger cannot be accessed in Logger`
l.context = "model"

l.info("test message 2")
// returns `INFO: controller: test message 2`

Now, no outside object can change the state of context. This solution still has two problems though: Logger’s internal methods can still modify context resulting in broken referential transparency; we loose automatically generated getters since context is private.

Is there a better solution? To achieve full referential transparency and still be able to use class constructors we have to use read-only variables. Thankfully, it’s the default behavior for constructors in Scala:

class Logger(context: String = "generic")

This is equivalent to:

class Logger(val context: String = "generic")

This way instance variables can’t be modified internally or externally and we still have read access to them. What happens if at some point we need to change a property of a class instance? Like with anything in functional programming, the only way to do it and not break referential transparency is to create another object.

If you spend most of your days in the world of imperative programming the lack of static class members in Scala may come as a surprise. Instead of static class members it provides a singleton construct called object where all members are static. This separation of concerns may feel redundant at first but it actually is very useful since it reduces complexity in large code bases.

Objects

Object is a special type of class. It can only be instantiated once, which makes it a singleton. All object members are static.

// `JavaRedisAdapter` is just a possible Java implementation
// of a Redis adapter.

object Cache extends JavaRedisAdapter {
  val a = new RedisInMemoryAdapter
  
  def get(k: String): Option[String] = {
    a.get(k) match {
      case v: String => Some(v)
      case _ => None
    }
  }
  
  def set(k: String, v: Option[String]): Boolean = storage.set(k, v)
}

Cache.set("testKey", Some("testValue"))
// returns true

Cache.get("testKey")
// returns Some("testValue")

Cache.get("nonExistentValue")
// returns None

Objects in Scala are a built-in implementation of the singleton pattern. Logging and caching utilities are great use cases for singletons. Some choose to implement them with dependency injection but in most cases it’s an overkill. Another great use for objects is shared resource control. It can be used for the management of database connection pools, threads, or writing to files on disk.

Singleton

The singleton pattern restricts the instantiation of a class to one object. It can be useful when exactly one object is needed in the implementation.

At first glance, objects behave like traditional singletons but there is more to it than meets the eye, which makes them really attractive and usable compared to anti-pattern-y singletons in Java.

Objects are polymorphic and can inherit from classes or interface-like structures. You can see it in the previous example where we extended an arbitrary Java class for our Scala application. Object polymorphism means that we can easily inject objects as dependencies without tight coupling headaches and global state that exists in Java singletons.

Another distinction between objects and traditional singletons is that objects take care of the boilerplate code. It’s hard to overstate this. Imagine that you don’t need to implement the getInstance() method for every one of your singletons nor do you need to instantiate a singleton instance in other classes—much less room for errors and bugs! This approach promotes the single responsibility principle and makes for easy testing.

Single Responsibility Principle

The single responsibility principle states that every component should have responsibility over a single part of the functionality. More importantly, responsibility should be entirely encapsulated by the class.

Traits

If you used a language that allows mixins (e.g., modules in Ruby), traits should look familiar. In a nutshell, traits are class components that can be stacked together. They are similar to Java interfaces and are allowed to have method implementations (as of 1.8 Java also allows default interface methods that can contain implementations).

One big difference between classes and traits is that traits don’t support constructor parameters but have type parameters. Traits are supposed to be very minimal and have only one responsibility. This way multiple traits can be stacked together. This principle is called composition over inheritance and is generally a good practice to follow because it allows for better extensibility and flexibility of programs.

Composition over Inheritance

Composition over inheritance is an approach to polymorphism and code reuse that states that classes should use composition by containing method implementations from other components that implement the desired functionality rather than inherit from a parent class.

Here is an example that composes two traits in one object:

trait Boldable {
  def bold(text: String) = s"**$text**"
  def unbold(text: String) = ???
}

trait Italicizable {
  def italicize(text: String) = s"*$text*"
  def unitalicize(text: String) = ???
}

object MarkdownWrapper extends Boldable with Italicizable {
  def boldAndItalic(text: String) = bold(italicize(text))
}

MarkdownWrapper.wrapWithBoldAndItalic("Scala rocks!")
// returns `***Scala rocks!***`

How does Scala solve the multiple inheritance problem in traits also known as the diamond problem?

The Diamond Problem

The diamond problem is an ambiguity that arises when multiple inheritance is involved. If class Foo inherits from two components Bar and Baz that contain the same method then which version of the method does Foo inherit? Languages supporting multiple inheritance solve this problem in different ways.

The solution is fairly straightforward: overridden members take precedence from right to left. The implementation on the right always wins over the implementation on the left (i.e., in Foo extends Bar with Baz Baz overrides Bar methods). This is different from how Java handles default interface methods. In Java, the program will fail to compile if a class implements two interfaces with the same default methods; the class itself would have to provide an implementation in order to resolve this problem.

Here is a more concrete example of multiple trait inheritance:

trait GenericStream {
  val stream: Stream[Int]
}

trait IntegerStreams extends GenericStream {
  override val stream = Stream.from(1)
  val odds: Stream[Int] = Stream.from(1, 2)
  val events: Stream[Int] = Stream.from(2, 2)
}

trait FibonacciStream extends GenericStream {
  override val stream = 0 #:: stream.scanLeft(1)(_ + _)
}

object FunkyMath extends IntegerStreams with FibonacciStream {
  def generateIntegers(n: Int) = stream.take(n).toList
}

In this case, generateIntegers uses the FibonacciStream stream implementation. To get multiple inheritance to work we must use the override keyword otherwise we’ll get a compile exception about conflicting members.

Case Classes

Case classes are immutable data-holding entities that are used for pattern matching and algebraic data types. They can also be used like regular classes.

Algebraic Data Types (ADTs)

Algebraic data types are types formed by composing other types. Here is an example of a tree structure implemented through ADTs that uses case classes:

// sealed traits can't be extended outside of the file
// they are defined in
sealed trait Tree[A]
case class EmptyTree[A]() extends Tree[A]
case class Node[A](value: A,
                   left: Tree[A],
                   right: Tree[A]) extends Tree[A]

Case classes have all properties of regular classes with a few extras:

  • Case classes can be initialized with a shortcut without the new keyword. val f = new Foo("hi") becomes val f = Foo("hi").

  • Case classes have a built-in toString method that generates a string with the case class name and its constructor arguments.

  • Case classes have a built-in equality implementation. This means that you can compare two instances of the same case class like this: Foo(25) == Foo(26) without implementing equals by hand.

  • Default implementation of hashCode is based on case class constructor arguments.

  • Case classes have a built-in copy method that makes a copy of a case class instance with custom parameter values rewritten. For example Foo(25).copy(param = 26) will return a new instance of Foo with a modified param.

Here is an example of how case classes can be used in the real world:

sealed trait Resource {
  def fullPath: String
}

case class Folder(name: String,
                  path: Option[String] = None) extends Resource {
  def fullPath: String = path match {
    case Some(p) => List(p, name).mkString("/")
    case None => s"./$name"
  }
}

case class File(name: String,
                folder: Option[Folder] = None) extends Resource {
  def fullPath: String = folder match {
    case Some(f) => List(f.fullPath, name).mkString("/")
    case None => s"./$name"
  }
}

val resources = Seq[Resource](
  File("ex1.scala", Some(Folder("example", Some("~/dev")))),
  Folder("tmp"),
  Folder("bin", Some("/usr")),
  File(".zshrc")
)

resources foreach {
  case f: File => println(s"File: ${f.fullPath}")
  case f: Folder => println(s"Folder: ${f.fullPath}")
}

// the above code outputs:
//
// File: ~/dev/example/ex1.scala
// Folder: ./tmp
// Folder: /usr/bin
// File: ./.zshrc

Here two case classes Folder and File inherit from a trait with an abstract fullPath method. Then we define a collection of Resources that can contain Resources, Folders, and Files. Finally, we loop over the collection and match its elements based on case classes. In this example they act like algebraic data types that allowing us to implement a simple domain-specific language. It’s a powerful tool that can save tons of boilerplate code and make programs more readable and verifiable.

How can a case class be magically instantiated without the new keyword? What lies beyond this syntactic sugar? All case classes have companion objects that are automatically created for default case class constructors in the background. In the Folder case class example it looks like this:

object Folder {
  def apply(name: String,
            folder: Option[Folder] = None) = new File(name, folder)
}

When the Folder("tmp") call is being compiled the Folder object gets created and injected. Then the apply method (also called the factory method) creates an instance of a case class. All of this happens in the background invisible to the programmer but we can always override the default companion object with our own version. It’s useful when we want to do something with constructor arguments outside of the case class implementation. Here is a more advanced example where we pass two different entities to the case class constructor and implement validation rules in the companion object:

import scala.util.{Failure, Success, Try}

case class Product(name: String, url: Option[String])
case class User(name: String, fullName: String, age: Int)

case class Customer(name: String, project: String, age: Int) {
  def this(u: User, p: Product) = this(u.name, p.name, u.age)
}

// custom companion object
object Customer {
  def apply(u: User, p: Product): Option[Customer] = Try {
    require(u.age >= 0)
    require(u.name.matches("^[a-zA-Z0-9]*$"))

    new Customer(u, p)
  } match {
    case Success(c) => Some(c)
    case Failure(e) => None
  }
}

Customer(
  User("vasily", "Vasily Vasinov", 26),
  Product("Amazon EC2", None)
)

// returns `Some(Customer(vasily,Amazon EC2,26))`

Here we defined a custom constructor in the Customer case class that calls the primary constructor with values pulled from Product and User instances. Then we created a custom companion object with an apply method that calls a custom constructor from the case class. In this method we perform simple validations and return Some(Customer) on success and None on failure while keeping validation logic completely decoupled from the case class.

Abstract Classes

Abstract classes in Scala are similar to Java: they can only be inherited from and never instantiated. Abstract classes can also have constructors and type parameters.

They are used when there is a need for a Scala implementation of an abstract class that will be called from Java or if an abstract class needs to be distributed in a compiled form.

Another use case for using an abstract class is when we need a base class that requires constructor arguments (traits don’t support constructor arguments). Establishing requirements for dependency injection is one possible scenario:

abstract class BaseModel(db: Database, table: String) {
  val id: Int // abstract property
  val t = TableHelper(db, table)

  def toJson: String // abstract method
  
  def get = t.get(id)
  
  def save: Option[Int] = t.save(toJson)
  
  def update: Boolean = t.update(toJson)
  
  def delete: Boolean = t.delete(id)
}

case class PostTemplate(title: String, body: String)

class Post(id: Int,
           template: PostTemplate) extends BaseModel(new Database, "posts") {
  def toJson =
    s"""{"id": $id, "title": "${template.title}", "body": "${template.body}"}"""
}

val p = new Post(1, PostTemplate("Scala OOP Galore", "Scala is a hybrid..."))

p.toJson

// returns `{"id": 1, "title": "Scala OOP Galore", "body": "Scala is a hybrid..."}`

Implicit Classes

Implicit classes are powerful extension tools to be used with other concrete classes. If you used Ruby, you’ll recognize implicit classes as an alternative to monkey patching. In Ruby, if we wanted to extend an existing class without introducing new classes the following implementation would be acceptable:

class Fixnum
  def odd?
    self % 2 != 0
  end
  
  def even?
    self % 2 == 0
  end
end

21.even?
# returns false

Examples like that are usually used to show the power of Ruby (as well as its weakness). It’s certainly very impressive but Scala has something very similar (but better) to offer. Consider the following example:

object IntSandbox extends App {
  implicit class IntUtils(val x: Int) {
    def isOdd = x % 2 != 0
    
    def isEven = x % 2 == 0
  }

  21.isEven // returns false
}

We just defined an implicit IntUtils class with a single constructor value of type Int. This instructs the compiler to implicitly convert any Int in scope to IntUtils that inherits all Int members. The difference between Scala and Ruby here is that Scala only applies the implicit in the current scope, which means that we can import our implicit classes wherever we need them without littering in the global namespace. This allows for less collisions and more compact DSLs.

You have to remember about three limitations when working with implicit classes:

  • Implicit classes have to be defined inside of another trait, class, or object.
  • Implicit classes can only have one non-implicit argument in the constructor.
  • You can’t have another object or member in scope with the same name, which implies that case classes can’t be implicit since they have a companion object with the same name.

Join Discussion

Subscribe to our Grokked Weekly newsletter to receive excellent engineering articles and the latest lessons from Grok Academy.