Scala

Scala Tutorial – objects, classes, inheritance, traits, Lists with multiple related types, apply

Preface

This is part 9 of tutorials for first-time programmers getting into Scala. Other posts are on this blog, and you can get links to those and other resources on the links page of the Computational Linguistics course I’m creating these for. Additionally you can find this and other tutorial series on the JCG Java Tutorials page.

This tutorial is about object-oriented programming with Scala. Most of what we’ve seen so far has been programming with functions and using basic types, like Int, Double, and String, and with predefined types like List and Map. As it turns out, these are all classes, or types of Scala data structures that allow one to create objects, or instances of the type. This tutorial will not give a broad introduction to object-oriented programming, but it will give some practical examples of classes and objects and how to use them. I apologize in advance for some sloppiness in the presentation of object-oriented concepts; the intent is to get across the ideas for beginners mainly through intuitive examples without being mired in lots of technical details. See the Wikipedia page on object-oriented programming for more detail.

Note that the definitions of objects and classes in this tutorial are most easily viewed as plain text, out of the REPL. So, I’ll put a piece of code into the text, and you should add it to your own REPL (by simply cutting and pasting) in order to be able to follow along.

Objects

At its core, an object can be thought of as a structure that encapsulates some data and functions. Let’s start with an an example of an object representing a person and some of their possible attributes.

object JohnSmith {
  val firstName = "John"
  val lastName = "Smith"
  val age = 37
  val occupation = "linguist"

  def fullName: String = firstName + " " + lastName

  def greet (formal: Boolean): String = {
    if (formal)
      "Hello, my name is " + fullName + ". I'm a " + occupation + "."
    else
      "Hi, I'm " + firstName + "!"
  }

}

If you put this into the Scala REPL, you’ll be able to access the fields (firstName, lastName, age, and occupation) and the functions (fullName and greet).

scala> JohnSmith.firstName
res0: java.lang.String = John

scala> JohnSmith.fullName
res1: String = John Smith

scala> JohnSmith.greet(true)
res2: String = Hello, my name is John Smith. I'm a linguist.

scala> JohnSmith.greet(false)
res3: String = Hi, I'm John!

So, at its most basic level, an object is just that: a collection of values and functions (also often called methods). You can access any of those values or functions by giving the name of the object followed by a period followed by the value or function you want to use. This can be useful for organizing such collections, but it also leads to many more possibilities, as we’ll see.

We might of course be interested in having the information about another person encapsulated in this way. We could do this by mimicking the definition for John Smith.

object JaneDoe {
  val firstName = "Jane"
  val lastName = "Doe"
  val age = 34
  val occupation = "computer scientist"

  def fullName: String = firstName + " " + lastName

  def greet (formal: Boolean): String = {
    if (formal)
      "Hello, my name is " + fullName + ". I'm a " + occupation + "."
    else
      "Hi, I'm " + firstName + "!"
  }

}

After adding the above code to the REPL, now Jane Doe can greet us.

scala> JaneDoe.greet(true)
res4: String = Hello, my name is Jane Doe. I'm a computer scientist.

scala> JaneDoe.greet(false)
res5: String = Hi, I'm Jane!

Of course, I created the JaneDoe object by doing a copy-and-paste and then replacing the fields with Jane Doe’s information. This leads to a lot of wasted effort: the fields are the same, but the values are different, and the functions are completely identical. If you want to change something about the way greetings are made, you’d have to update it across all of the objects.

More importantly, these two objects are completely distinct from one another: one cannot put them in a list and map a function over that list. Consider the following failed attempt.

scala> val people = List(JohnSmith, JaneDoe)
people: List[ScalaObject] = List(JohnSmith$@698fcb66, JaneDoe$@5f72cbae)

scala> people.map(person => person.firstName)
<console>:11: error: value firstName is not a member of ScalaObject
people.map(person => person.firstName)
                                          ^

The only thing that Scala knowns about JohnSmith and JaneDoe is that they are ScalaObjects. That means that a list of such objects can basically just contain them and allow you to move them around as a group. So, something more is needed to make these collections more useful and more general.

Classes

With the list above, what we’d like to have is a List[Person], where Person is a type that has known fields and functions. We can accomplish this by defining a Person class and then defining John and Jane as members of that class. This also reduces the cut-and-paste duplication problem noted earlier. Here’s what it looks like.

class Person (
  val firstName: String,
  val lastName: String,
  val age: Int,
  val occupation: String
) {

  def fullName: String = firstName + " " + lastName

  def greet (formal: Boolean): String = {
    if (formal)
      "Hello, my name is " + fullName + ". I'm a " + occupation + "."
    else
      "Hi, I'm " + firstName + "!"
  }

}

The class keyword indicates that this is a class definition and Person is the name of the class. The next part of the definition is a set of parameters to the class that allow us to construct objects that are instances of the class — in other words, they are placeholders that allow us to use the Person class as a factory for creating Person objects. We do this by using the new keyword, giving the name of the class and supplying the values for each of the parameters. For example, here’s how we can create John Smith now.

scala> val johnSmith = new Person("John", "Smith", 37, "linguist")
johnSmith: Person = Person@1979d4fb

Just as we could with the one-off standalone JohnSmith object previously, we can now access the fields and functions.

scala> johnSmith.age
res8: Int = 37

scala> johnSmith.greet(true)
res9: String = Hello, my name is John Smith. I'm a linguist.

Defining other people is now easy, and doesn’t require any cutting-and-pasting.

scala> val janeDoe = new Person("Jane", "Doe", 34, "computer scientist")
janeDoe: Person = Person@7ff5376c

scala> val johnDoe = new Person("John", "Doe", 43, "philosopher")
johnDoe: Person = Person@6544c984

scala> val johnBrown = new Person("John", "Brown", 28, "mathematician")
johnBrown: Person = Person@4076a247

These Person objects can now be put into a list together, giving us a List[Person] that allows mapping to retrieve specific values, like first names and ages, and performing computations like calculating the average age of the individuals in the list.

scala> val people = List(johnSmith, janeDoe, johnDoe, johnBrown)
people: List[Person] = List(Person@1979d4fb, Person@7ff5376c, Person@6544c984, Person@4076a247)

scala> people.map(person => person.firstName)
res10: List[String] = List(John, Jane, John, John)

scala> people.map(person => person.age)
res11: List[Int] = List(37, 34, 43, 28)

scala> people.map(person => person.age).sum/people.length.toDouble
res12: Double = 35.5

We can sort them according to age.

scala> val ageSortedPeople = people.sortBy(_.age)
ageSortedPeople: List[Person] = List(Person@4076a247, Person@7ff5376c, Person@1979d4fb, Person@6544c984)

scala> ageSortedPeople.map(person => person.fullName + ":" + person.age)
res13: List[java.lang.String] = List(John Brown:28, Jane Doe:34, John Smith:37, John Doe:43)

We can also group people by first name, last name, etc.

scala> people.groupBy(person => person.firstName)
res14: scala.collection.immutable.Map[String,List[Person]] = Map(Jane -> List(Person@7ff5376c), John -> List(Person@1979d4fb, Person@6544c984, Person@4076a247))

scala> people.groupBy(person => person.lastName)
res15: scala.collection.immutable.Map[String,List[Person]] = Map(Brown -> List(Person@4076a247), Smith -> List(Person@1979d4fb), Doe -> List(Person@7ff5376c, Person@6544c984))

With this, we can have all the Johns greet us.

scala> people.groupBy(person => person.firstName)("John").foreach(john => println(john.greet(true)))
Hello, my name is John Smith. I'm a linguist.
Hello, my name is John Doe. I'm a philosopher.
Hello, my name is John Brown. I'm a mathematician.

Standalone objects

Above, we saw how to create instances of the Person class by using the new keyword and assigning the resulting object to a variable. We can come back full circle to the first JohnSmith object we created, which was a standalone ScalaObject. We can instead create such a standalone object by extending the Person class.

scala> object ThomYorke extends Person("Thom", "Yorke", 43, "musician")
defined module ThomYorke

scala> ThomYorke.greet(true)
res25: String = Hello, my name is Thom Yorke. I'm a musician.

By extending the Person class to create the object, we are saying that the object is a kind of Person — see more on inheritance below. So, ThomYorke is a Person object, like the others we created, but it is for a different use case that we’ll see more of in the next tutorial. For now, I’ll summarize, very roughly, by saying that the ThomYorke object can be made more accessible by other code that might be using my code, while the johnSmith and janeDoe objects are going to be more locally contained.

Inheritance

The standalone objects lead us naturally to the idea of inheritance. In many domains, there are natural hierachies of types, such that properties of a super type are inherited by its subtypes (e.g. fish have gills and swim, so salmon have gills and swim). For example, we could have a Linguist type that is a kind of Person, a ComputerScientist type that is a kind of Person, and so on. To model this, we create one class that extends another and possibly provides some additional parameters, such as the following definition of a Linguist sub-type of Person.

class Linguist (
  firstName: String,
  lastName: String,
  age: Int,
  val speciality: String,
  val favoriteLanguage: String
) extends Person(firstName, lastName, age, "linguist") {

  def workGreeting =
    "As a " + occupation + ", I am a " + speciality + " who likes to study the language " + favoriteLanguage + "."

}

The Linguist class has its own parameter list: some of these, like firstName, lastName, and age, are passed on to Person, and there are new parameter fields speciality and favoriteLanguage. The extends portion of the definition passes on the relevant parameters needed to construct all the information to make a Person, and for a Linguist, it directly sets the occupation parameter to be “linguist” — thus, we don’t need to provide that when we construct a Linguist, such as Noam Chomsky.

scala> val noamChomsky = new Linguist("Noam", "Chomsky", 83, "syntactician", "English")noamChomsky: Linguist = Linguist@54c0627f

Having defined a Linguist object in this way, we can ask it to give its work greeting.

scala> noamChomsky.workGreeting
res26: java.lang.String = As a linguist, I am a syntactician who likes to study the language English.

We can also access fields and functions of Person objects, like age and greet.

scala> noamChomsky.age
res27: Int = 83

scala> noamChomsky.greet(true)
res28: String = Hello, my name is Noam Chomsky. I'm a linguist.

Of course, the Linguist-specific fields like favoriteLanguage are accessible too.

scala> noamChomsky.favoriteLanguage
res29: String = English

The observant reader will have noticed that some of the parameters are prefaced with val and others are not. We’ll get back to that distinction a bit later.

Traits

We could of course now go on to define a ComputerScientist class that would also have workGreeting function, but the Linguist.workGreeting and ComputerScientist.workGreeting would be entirely separate. To enable this, we can use traits, which are like classes, but which define an interface of functions and fields that classes can supply concrete values and implementations for. (Note: traits can also define concrete fields and functions, so they aren’t limited to placeholder functions as we show below.)

As an example, here’s a Worker trait, which simply defines a function workGreeting and declares that it must return a String.

trait Worker {
  def workGreeting: String
}

The Linguist class defined earlier already provides an implementation of that function. To allow a Linguist to be considered as a type of Worker, we add with Worker after extending Person.

class Linguist (
  firstName: String,
  lastName: String,
  age: Int,
  val speciality: String,
  val favoriteLanguage: String
) extends Person(firstName, lastName, age, "linguist") with Worker {

  def workGreeting =
    "As a " + occupation + ", I am a " + speciality + " who likes to study the language " + favoriteLanguage + "."

}

This is called “mixing in” the trait Worker, because the Linguist class mixes in the fields and functions of Worker with those of Person.

Note that we can also create classes that simply extend a trait like Worker.

class Student (school: String, subject: String) extends Worker {
  def workGreeting = "I'm studying " + subject + " at " + school + "!"
}

We can now create a Student object and request their greeting.

scala> val anonymousStudent = new Student("The University of Texas at Austin", "history")
anonymousStudent: Student = Student@734445b5

scala> anonymousStudent.workGreeting
res32: java.lang.String = I'm studying history at The University of Texas at Austin!

Notice that the parameters school and subject were not preceded by val in the definition of Student. That means that they are not member fields of the Student class, which means that they cannot be accessed externally. For example, attempting to access the value provided for school for anonymousStudent fails.

scala> anonymousStudent.school
<console>:11: error: value school is not a member of Student
anonymousStudent.school

Of course, internally, Student can use the values provided to such parameters, for example in defining the result of workGreeting. This sort of encapsulation hides properties of the objects of a class from code that is outside the class; this strategy can help reduce the degrees of freedom available to users of your code so that they only use what you want them to. In general, if others don’t need to use it, you shouldn’t make it available to them.

Returning to classes that are both Persons and Workers, when we define a ComputerScientist, we do a similar extends … with declaration as we did for Linguist.

class ComputerScientist (
  firstName: String,
  lastName: String,
  age: Int,
  val speciality: String,
  favoriteProgrammingLanguage: String
) extends Person(firstName, lastName, age, "computer scientist") with Worker {

  def workGreeting =
    "As a " + occupation + ", I work on " + speciality + ". Much of my code is written in " + favoriteProgrammingLanguage + "."

}

Let’s create Andrew McCallum as a ComputerScientist object.

scala> val andrewMcCallum = new ComputerScientist("Andrew", "McCallum", 44, "machine learning", "Scala")
andrewMcCallum: ComputerScientist = ComputerScientist@493cd5ba

scala> andrewMcCallum.workGreeting
res31: java.lang.String = As a computer scientist, I work on machine learning. Much of my code is written in Scala.

Because we redefined Linguist to be a Worker, we need to recreate Noam Chomsky using the new definition. (The creation looks the same as before, but it uses the new class definition that has been updated in the REPL.)

scala> val noamChomsky = new Linguist("Noam", "Chomsky", 83, "syntactician", "English")
noamChomsky: Linguist = Linguist@6fccaf14

A minor thing to note: the speciality field of ComputerScientist is disconnected from that of Linguist, so there is no particular expectation of consistency of use across the two: for Linguist it is a description of a person working in a sub-area but for ComputerScientist is a description of a sub-area.

So, what happens if we put noamChomsky and andrewMcCallum in a List together?

scala> val professors = List(noamChomsky, andrewMcCallum)
professors: List[Person with Worker] = List(Linguist@6fccaf14, ComputerScientist@493cd5ba)

Scala has created a list with type List[Person with Worker]; this is the most specific type that is valid for all elements of the list. It means we can treat all of the elements as Persons, e.g. accessing their occupation (which is a member field of Person).

scala> professors.map(prof => prof.occupation)
res34: List[String] = List(linguist, computer scientist)

And we can treat each element of the list as a Person and a Worker, e.g. printing out their fullName (from Person) and their workGreeting (from Worker).

scala> professors.foreach(prof => println(prof.fullName + ": " + prof.workGreeting))
Noam Chomsky: As a linguist, I am a syntactician who likes to study the language English.
Andrew McCallum: As a computer scientist, I work on machine learning. Much of my code is written in Scala.

We cannot, however, access fields and functions that are specific to Linguists or ComputerScientists, such as favoriteLanguage from Linguist.

scala> professors.map(prof => prof.favoriteLanguage)
<console>:15: error: value favoriteLanguage is not a member of Person with Worker
professors.map(prof => prof.favoriteLanguage)

It is easy to see why Scala has this behavior: even though that would have been valid for noamChomsky, it would not be for andrewMcCallum (according to the way we defined Linguist and ComputerScientist).

Matching on types in polymorphic Lists

Consider what happens when the anonymousStudent is in a list with the professors.

scala> val workers = List(noamChomsky, andrewMcCallum, anonymousStudent)
workers: List[ScalaObject with Worker] = List(Linguist@6fccaf14, ComputerScientist@493cd5ba, Student@734445b5)

The Person type is gone, and we now have a list of a more general type ScalaObject with Worker. Now we can only use the workGreeting method from Worker.

However, it is worth pointing out that match statements come in handy when you have collections of heterogenous objects. For example, put the following code into the REPL.

val people = List(johnSmith, noamChomsky, andrewMcCallum, anonymousStudent)

people.foreach { person =>
  person match {
    case x: Person with Worker => println(x.fullName + ": " + x.workGreeting)
    case x: Person => println(x.fullName + ": " + x.greet(true))
    case x: Worker => println("Anonymous:" + x.workGreeting)
  }
}

The result is the following (remember that johnSmith was never defined as a Linguist — he was defined as a Person whose occupation is “linguist”).

John Smith: Hello, my name is John Smith. I'm a linguist.
Noam Chomsky: As a linguist, I am a syntactician who likes to study the language English.
Andrew McCallum: As a computer scientist, I work on machine learning. Much of my code is written in Scala.
Anonymous:I'm studying history at The University of Texas at Austin!

So, we can switch our behavior by matching to more specific types using Scala’s pattern matching.

The apply function

Scala provides a simple but incredibly nice feature: if you define an apply function in a class or object, you don’t actually need to write “apply” in order to use it. As an example, the following object adds one to an argument supplied to its apply method.

object AddOne {
  def apply (x: Int): Int = x+1
}

So, we can use it just like you’d normally expect.

scala> AddOne.apply(3)
res41: Int = 4

But, we can also do without the “.apply” portion and get the same result.

scala> AddOne(3)
res42: Int = 4

If a class has an apply method, then we can do the same trick with any object of that class.

class AddN (amountToAdd: Int) {
  def apply (x: Int): Int = x + amountToAdd
}

scala> val add2 = new AddN(2)
add2: AddN = AddN@43ca04a1

scala> add2(5)
res43: Int = 7

scala> val add42 = new AddN(42)
add42: AddN = AddN@83e591f

scala> add42(8)
res44: Int = 50

As it turns out, you’ve been using apply methods quite often, without knowing it! When you have a List and you access an element by index, you’ve used the apply method of the List class.

scala> val numbers = 10 to 20 toList
numbers: List[Int] = List(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

scala> numbers(3)
res46: Int = 13

scala> numbers.apply(3)
res47: Int = 13

Same thing for accessing values using keys in a Map, and similarly for many other of the classes you’ve been using in Scala so far.

Wrap-up

This tutorial has covered the basics of object-oriented programming in Scala. Hopefully, it is enough to give a decent sense of what objects and classes are and how you can do things with them. There is much much more to be learned about them, but this should be sufficient to get you started so that further study can be done meaningfully. It is important to understand these concepts since Scala is object-oriented from the ground up. In fact, in many of the previous tutorials, I’ve at times gone through some extra hoops to try to describe what is going on without having to talk about object-orientation. But now you can see things like Int, Double, List, Map, and so on for what they are: classes that contain particular fields and functions that you can use to get things done. You can now start coding your own classes to enable your own custom behaviors in your applications.

Reference: First steps in Scala for beginning programmers, Part 9 from our JCG partner Jason Baldridge at the Bcomposes blog.

Related Articles :

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button