Scala OOP Galore
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 post 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")
becomesval 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 implementingequals
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 exampleFoo(25).copy(param = 26)
will return a new instance ofFoo
with a modifiedparam
.
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 Resource
s that can contain Resource
s, Folder
s, and File
s. 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.