DEV Community

Avi Kaminetzky
Avi Kaminetzky

Posted on • Updated on

Scala - On Classes & Objects

I've put together a semi-beginnerish guide for some of the pesky parts of classes and objects in Scala, ideal for someone coming from from languages like JavaScript & Python, and has barely encountered compiled, strongly typed languages.

I attempt to
1. Give the simplest examples
2. Never ever dare compare Scala to Java
3. Avoid selling you on functional programming or Scala

Each section builds on the previous one.

Recommended Resources

These resources are not directly related to this tutorial, but are very useful for learning Scala and functional programming. without making assumptions about your
[insert from here: functional programming/type theory/object oriented programming/category theory/math phd/actor model/Java 8/reactive programming] prowess.

Traits

Traits are used to share interfaces and fields between classes... Classes and objects can extend traits but traits cannot be instantiated and therefore have no parameters. (docs)

Traits can have both abstract and concrete methods.

trait Person {
  // concrete method, can only be overridden by class if it's a def, not a var or val
  def name = "Tim"
  // abstract method, must be implemented in class
  def greeting: String
}

class Tim extends Person {
  def greeting: String = s"Welcome, ${name}"
}

val timClass = new Tim
timClass.greeting
// res0: String = Welcome, Tim
Enter fullscreen mode Exit fullscreen mode

You can override name in the Person trait:

class Tim extends Person {
  // override trait name.
  override val name: String = "Mr. Tim"
  def greeting: String = s"Welcome, ${name}"
}

val timClass = new Tim
timClass.greeting
// res0: String = Welcome, Mr. Tim
Enter fullscreen mode Exit fullscreen mode

Plain Objects

An object on its own is simply a way to package together data and functions. Technically, it's a singleton instance of an anonymous class.

trait Person {
  val name: String
}

object Tim extends Person {
  val name = "Tim"
}

Tim.name
// res0: String = Tim
Enter fullscreen mode Exit fullscreen mode

Another example from the App trait:

The App trait can be used to quickly turn objects into executable programs." (docs)

The verbose way:

object HelloWorld {
  def main(args: Array[String]): Unit = {
    val name = args(0)
    println(s"Hello, $name")
  }
}
// "Hello, dude"
Enter fullscreen mode Exit fullscreen mode

Without the boilerplate:

object HelloWorld extends App {
    val name = args(0)
    println(s"Hello, $name")
}
Enter fullscreen mode Exit fullscreen mode

To execute, run scala HelloWorld.scala dude from command line.

Option/Some

Any value that could potentially contain a null/empty value, has an option type, and when assigned, must be wrapped in Some.

val name: Option[String] = Some("Tim")
name.getOrElse("Anon")
// res0: String = Tim
Enter fullscreen mode Exit fullscreen mode

The getOrElse method will provide a default for when name is None. It will also reduce name from a Some("Tim") value, to a plain "Tim" value.

A nice article on idiomatic handling of Option values.

The introduction of Maybe can cause some initial discomfort. Users of Swift and Scala will know what I mean as it's baked right into the core libraries under the guise of Option(al). When pushed to deal with null checks all the time (and there are times we know with absolute certainty the value exists), most people can't help but feel it's a tad laborious. However, with time, it will become second nature and you'll likely appreciate the safety. After all, most of the time it will prevent cut corners and save our hides.

Writing unsafe software is like taking care to paint each egg with pastels before hurling it into traffic; like building a retirement home with materials warned against by three little pigs. It will do us well to put some safety into our functions and Maybe helps us do just that. (Mostly Adequate Guide to Functional Programming Ch 8)

Pattern Matching

Pattern matching in Scala, is like a switch statement on steroids.

val name: Option[String] = Some("Bob")

name match {
  // for an exact match on a Tim name
  case Some("Tim") => "Hi, Tim!"

  // for any other Some(value)
  case Some(name) => s"Hi ${name}"

  // default case for any other value
  case _ => "Anonymous has no name!"
}

// res0: String = Hi Bob
Enter fullscreen mode Exit fullscreen mode

Object's apply() and unapply() Methods

The way I think about these functions is that apply() is an object's built in constructor, while unapply() is how an object deconstructs, which particularly useful for pattern matching. These two functions are mutually exclusive, so you can define one without the other. (StackOverflow) Example from the docs.

object Person {
  def apply(name: String) = {
    s"Welcome--$name"
  }
  def unapply(name: String): Option[String] = {
    val nameSplit = name.split("--")
    // we can't be sure the value exists in the object
    if (nameSplit.tail.nonEmpty) Some(s"Goodbye--${nameSplit(1)}")
    else None
  }
}

// using apply() to construct the object
val dudePerson = Person("dude")
println(dudePerson)
// res0: Welcome--dude

// using unapply() to assign a variable
val Person(dudeName) = dudePerson
println(dudeName)
// res1: Goodbye--dude

// using unapply() for pattern matching
val matchTheDude = dudePerson match {
  case Person(name) => name
  case _ => "Goodbye--anonymous dude"
}
println(matchTheDude)
// res2: Goodbye--dude
Enter fullscreen mode Exit fullscreen mode

Class Companion Object

When an object shares the same name with a class, it is commonly referred to as a companion object. It is useful for storing class functionality which isn't tied to an instance of the class (think of them as static or class methods).

For a class/companion object named Person with a constructor of name: String, the object can access anything from the class by calling new Person("Tim").value, while the class can access anything from its object by calling Person.value. Even if the values are set to private.


class Person(val name: String) {
  // Person(Tim) instead of the default memory address Person@3018b152
  override def toString = s"Person($name)"
}

object Person {
  // static method to track number of people objects instantiated
  var count = 0

  def apply(name: String): Person = {
    count = count + 1
    // using the Person object as the constructor, allows you to call Person("Tim")         
    // without the need to write "new Person("Tim")"
    new Person(name)
  }

  def unapply(person: Person): Option[String] = Some(person.name)
}

// calling apply() to instantiate the Person class
val bob = Person("Bob")

// our custom toString function
println(bob.toString)
// res0: Person(Bob)

// the apply() increments the count variable for each new Person
val tim = Person("Tim")
Person.count
// res1: 2

// unapply() with pattern matching
val matchTim = tim match {
  case Person(name) => name
  case _ => "Sorry, no match"
}
matchTim
// res2: String = Tim
Enter fullscreen mode Exit fullscreen mode

Case Classes

case class is a type of class, which is useful for storing immutable data with minimal boilerplate. case object has similar functionality, but for a singleton class.

case class Person(title: Option[String] = None, name: String)

// comes with a companion object apply() method, no need for new Person("Dude")
val dude = Person(name="Dude")

// name parameter is set to an immutable val
dude.name
// res0: String = Dude

// sane toString method, as opposed to Person@721d791f weird hex stuff
dude.toString
// res1: String = Person(None,Dude)

val tim = Person(name="Tim")

// convenient copy method if you only need to change specific values
val timCopy = tim.copy(title=Some("Mr"))
// timCopy: Person = Person(Some(Mr),Tim)

// comes with a companion object unapply() method, useful for pattern matching
timCopy match {
  case Person(title, name) => title
  case _ => "No title"
}
// res2: Some(Mr)

// sane equals method which compares the content of case classes
dude.equals(tim)
// res3: Boolean = false

// class equality is usually done by comparing the memory address of each class 
// instance, that's why you get this weird behavior
class Guy(name: String)

val guy1 = new Guy("guy")
// guy1: Guy = Guy@5f46825d

val guy2 = new Guy("guy")
//guy2: Guy = Guy@17be95d

guy1 == guy2
//res4: Boolean = false
Enter fullscreen mode Exit fullscreen mode

Implicits

A nice example from here:

// probably in a library
class Prefixer(val prefix: String)
def addPrefix(s: String)(implicit p: Prefixer) = p.prefix + s

// then probably in your application
implicit val myImplicitPrefixer = new Prefixer("***")
addPrefix("abc")  // returns "***abc"
Enter fullscreen mode Exit fullscreen mode

The Play Framework controllers use implicits, so that any function call from an Action will gain access to the http request variable:

def action = Action { implicit request =>
   anotherMethod("Some para value")
   Ok("Got request [" + request + "]")
 }

def anotherMethod(p: String)(implicit request: Request[_]) = {
 // do something that needs access to the request
 }
Enter fullscreen mode Exit fullscreen mode

implicit comes in handy when converting case classes to and from Json with the Play Json library. This is simple way to get type-safe JSON when creating an API:

import play.api.libs.json.Json
case class Totals(
                   subTotal: Double = 0,
                   estimatedShippingAndInsurance: Double = 0,
                   estimatedTax: Option[Double] = Some(0),
                   orderTotal: Double = 0
                 )
// withDefaultValues is required if you want your case class defaults to be applied // to the Json conversion
object Totals { implicit val writeTotals = Json.using[Json.WithDefaultValues].format[Totals] }
Enter fullscreen mode Exit fullscreen mode

Class Structuring

In object oriented programming, we make a distinction between is-a and has-a. is-a is a candidate for inheritance.

A Product has-a salePrice and a regularPrice:

trait Product {
  val salePrice: Double
  val regularPrice: Double
}
Enter fullscreen mode Exit fullscreen mode

A computer is-a product in a store:

case class Computer(
                     salePrice: Double,
                     regularPrice: Double,
                     processor: String,
                     operatingSystem: String
                   ) extends Product
Enter fullscreen mode Exit fullscreen mode

Clothes is-a Product as well:

case class Clothes(
                     salePrice: Double,
                     regularPrice: Double,
                     size: Int,
                     material: String
                   ) extends Product
Enter fullscreen mode Exit fullscreen mode

Another point to consider, Product is of type Computer or Clothes.

A common pattern for is-or (from Essential Scala):

sealed trait TrafficLight {
// pattern matching on case objects
  def next: TrafficLight = this match {
    case Red => Green
    case Green => Yellow
    case Yellow => Red
  }
}
final case object Red extends TrafficLight
final case object Green extends TrafficLight
final case object Yellow extends TrafficLight

val myLight = Red.next
// res0: TrafficLight = Green
myLight.next
// res0: TrafficLight = Yellow
myLight.next.next
// res1: TrafficLight = Red
Enter fullscreen mode Exit fullscreen mode

sealed trait is used to tell the compiler to only expect TrafficLight of types Red, Green and Yellow. If we were to leave out Yellow while pattern matching, the compiler would complain that match may not be exhaustive. final is to forbid anyone form extending the Red, Green and Yellow classes.

Discussion (1)

Collapse
darbyo profile image
Rob Darby

Nice article, I like the initial point of not comparing Scala to Java, its hard to find a good explanation of something in Scala that doesn't refer to Java or mention something like "In Java you would do this ... "