Luet oppimateriaalin englanninkielistä versiota. Mainitsit kuitenkin taustakyselyssä osaavasi suomea. Siksi suosittelemme, että käytät suomenkielistä versiota, joka on testatumpi ja hieman laajempi ja muutenkin mukava.

Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit. Myös suomenkielisessä materiaalissa käytetään ohjelmaprojektien koodissa englanninkielisiä nimiä kurssin alkupään johdantoesimerkkejä lukuunottamatta.

Voit vaihtaa kieltä A+:n valikon yläreunassa olevasta painikkeesta. Tai tästä: Vaihda suomeksi.


Chapter 5.3: Objects as Functions, Classes as Objects

About This Page

Questions Answered: Are classes objects? Are objects classes? Is a function an object or is an object a function? Can I keep my head together?

Topics: apply methods. Companion objects. Factory methods.

What Will I Do? Read, mostly.

Rough Estimate of Workload:? Just half an hour, maybe?

Points Available: B5.

Related Projects: None.

../_images/person03.png

Introduction

In this relatively short chapter, we’ll continue to study just how ubiquitous objects are in Scala programs.

Let’s begin with this example code:

val myVector = Vector("first", "second", "third", "fourth", "fifth")res0: Vector[String] = Vector(first, second, third, fourth, fifth)
myVector.apply(3)res1: String = fourth
myVector(3)res2: String = fourth

Consider the following questions.

  1. You can obtain an individual element with myVector(index) or with the longer expression myVector.apply(index). Why are there two different-looking but fundamentally similar ways of doing the same thing?
  2. You learned to command objects via method calls, so myVector.apply(index) looks legitimate. But isn’t there something odd about the expression myVector(index)? It uses a reference to an object (from myVector) and somehow passes the value of index as a parameter to that object. The command has the appearance of a function call but seems more like an “object call”, if there is such a thing. It’s not possible to “run an object”, is it?
  3. And what about the command that creates a vector? For the most part, we’ve used new when instantiating classes; why don’t we do that for vectors?

The answers to these questions are intertwined. In part, they are specific to the Scala language. Let’s start untwining.

The Special Method apply

An object “as a function”

No, you can’t issue a command to “run an object” in the same sense that you can issue a command to run a function. An object has methods, though, and those methods you can invoke.

So what’s going on in myVector(index)? The answer lies in how the Scala language assigns a special meaning to the name apply.

An expression where an object reference is directly followed by parameters in round brackets is interpreted as a call to the object’s apply method. For instance, myVector(index) is an abbreviated form of the actual command myVector.apply(index).

The same works for Strings: "llama".apply(3) and "llama"(3) both mean the same thing and return the character m.

In fact, this is a general rule that doesn’t require vectors, strings, or any other specific type. If an object — any object — has a method named apply, then that method serves as a “default method” for that object: if you leave out the dot and the name, the object runs the default method.

Another way to think about it is that an apply method lets you use an object “as though it was a function”. Bear in mind, though, that calling myObject(params) actually instructs the object to run its apply method, not to run the entire object in some more generic sense. An object is not actually a function; this is just a shorthand for a method call.

Examples of apply methods

The apply methods on different classes do different things. The example you already saw is the apply on vectors, strings, and other collections in the Scala API. The API creators have deemed it convenient that we can access an element in a collection by writing simply myCollection(index), so they’ve named that method apply.

You can write an apply method on any class or singleton object. As you do so, you’re free to choose what parameters the method takes and how it behaves. Here’s a simple example:

object myObject {

  def apply(word: String, another: String) = {
    println(word + ", " + another + "!")
  }

}

Now both myObject.apply("Ave", "Munde") and myObject("Ave", "Munde") call the apply method, which prints out a greeting.

In O1, you generally won’t need to write apply methods of your own. Even so, it’s good to be aware of the concept so that you know what’s going on when you use vectors and strings and the like. This knowledge can also help you decipher error messages.

Suppose you have a variable x with the type Vector[String]. It stores a reference to a vector object that contains at least one element. Think about the following claims, experiment in the REPL as needed, and select all the correct ones.

Companion Objects, or “Classes as Objects”

Let’s turn to another subject, during which apply will resurface.

An example class

Say we’re creating a class Customer and intend to number the class’s instances so that every instance is associated with a unique positive integer number.

The pseudocode below outlines a solution that resembles how many beginner programmers first attempt to solve this problem.

class Customer(val name: String) {

  Use a stepper variable createdInstanceCount to record how many customers exist; initially 0.
  When a new customer is created, increment the stepper by one.
  val number = the stepper’s value; that is, the number of instances so far

  override def toString = "#" + this.number + " " + this.name

}

Here is a Scala equivalent of the pseudocode:

class Customer(val name: String) {

  private var createdInstanceCount = 0
  this.createdInstanceCount += 1
  val number = this.createdInstanceCount

  override def toString = "#" + this.number + " " + this.name

}

However, when we use this class, it doesn’t do what we intended. Consider the code and the following animation of its execution to figure out what the problem is.

Which of the following claims about our example class are correct? Select all that apply. We intend for number and createdInstanceCount to work as discussed above.

Attributes at the class level

Since Chapter 2.3, we’ve been fostering the idea that a class describes the data type of a particular sort of objects. A class defines the attributes that its instances have; each object has its own copies of the instance variables defined on the class. Which is precisely why our attempt at numbering the customers failed.

Sometimes, we want to associate attributes or operations with the general concept that a class represents. That is, we want to attach a variable or a function to the class itself rather than every instance of the class separately. Our customer counter is just such a variable: although each customer instance has its own number, the total number of instances is not an attribute of any of the individual instances but the class as a whole.

We would like our customer example to work more like this:

We’d like to manipulate the class as if it, too, was an object — not one of the customer objects but an object that represents the general concept of customer and that provides the single counter variable we need.

In Scala, a class is not actually an object as such and cannot be used quite as illustrated above. What we can do instead is give the class a “friend” that is an object and enables us to implement the above algorithm.

Exposed: Intimate relationship between class and object

Here is a version of the program that works:

object Customer {
  private var createdInstanceCount = 0
}

class Customer(val name: String) {
  Customer.createdInstanceCount += 1
  val number = Customer.createdInstanceCount

  override def toString = "#" + this.number + " " + name

}
In addition to the class Customer we define a companion object (kumppaniolio) for the class. A companion object is a singleton object that has been given precisely the same name as a class and that is defined in the same file with that class.

The companion object Customer is not an instance of the customer class like the objects you create with new Customer(name) are. The companion object’s type is not Customer! The companion object is a distinct object whose attributes are associated with the Customer concept in general.

The companion object Customer has the variable createdInstanceCount. Only a single copy of this variable exists in memory, since the customer object is a singleton (see animation below). This contrasts with the names and numbers of the various customer instances. Similarly, the counter is initialized to zero just once when the companion object is created.
We specify that whenever we create a new object of type Customer, we increment the companion object’s createdInstanceCount variable by one, then copy its new value in the customer object’s instance variable number.
The Customer class and its companion object are “friends” and have access to each other’s private members.

As noted, companion objects are singleton objects. More generally, we can say that all of Scala’s singleton objects are either:

  1. companion objects, which share a name with a class defined in the same file; or
  2. standalone objects, which have no class as a companion but serve some other purpose in the program.

Nearly all the singleton objects that you’ve seen in O1 so far have been standalone objects.

For an additional example of a companion object, take a look at StarCoords.scala in the Stars project.

Methods in a companion object

You can define methods on a companion object just as you can define methods on any singleton. Just to illustrate the point, let’s add a method to our companion object:

object Customer {

  private var createdInstanceCount = 0

  def printCount() = {
    println("So far, " + this.createdInstanceCount + " customer objects have been created.")
  }

}

Now we can write Customer.printCount() to print out the report.

The type of a companion

As noted, a companion object isn’t an instance of the class whose companion it is and doesn’t have the data type defined by that class.

Like other singleton objects (Chapter 2.3), each companion object has its own data type that isn’t shared by any other object. The REPL session below illustrates the difference between the types of Customer instances and the Customer companion object.

new Customer("Eugenia Enkeli")res3: Customer = #1 Eugenia Enkeli
new Customer("Teija Tonkeli")res4: Customer = #2 Teija Tonkeli
Customerres5: Customer.type = Customer$@185ea23
Customer is the type of all the instances of class Customer.
Customer.type is the type of class Customer’s companion object (and no other object).

static?

To readers who have previously programmed in Java or related languages: the variables and methods that are defined as static in those languages tend to be defined in singleton objects (and companion objects especially) in Scala. In Scala, you have no need for (and the language does not provide) a static modifier, since everything is done with objects.

Uses for companion objects

An instance counter for a class is a classic introductory example that serves to clarify the basic concept of a companion object. Here are some other uses for companion objects that are more common in practice:

  1. Constants: a companion object is a nice place for storing constants (Chapter 2.6). Many constants pertain to a class in general and are not instance-specific. For example, the StarCoords companion object defines the constants MinValue and MaxValue that represent the edges of the coordinate system (-1.0 and +1.0).
  2. Auxiliary functions: when you need to define a function that’s associated with a class but isn’t a method on the class’s instances, you can write the function as a method on the class’s companion object.
  3. Factory methods:

On Factory Methods

Let’s add another method, createNew, to our Customer companion:

object Customer {

  private var createdInstanceCount = 0

  def printCount() = {
    println("So far, " + this.createdInstanceCount + " customer objects have been created.")
  }

  def createNew(name: String) = new Customer(name)

}

We can now write, say, Customer.createNew("Sasha") to obtain a new customer object just as if we had written new Customer("Sasha").

Such a method is called a factory method (tehdasmetodi). Its job is simply to generate and return a new object.

Note that we defined the factory method on the Customer companion object, not the Customer class, because creating a new customer is not an operation on existing customer objects.

Let’s edit our factory method a bit and name it apply instead:

object Customer {

  private var createdInstanceCount = 0

  def printCount() = {
    println("So far, " + this.createdInstanceCount + " customer objects have been created.")
  }

  def apply(name: String) = new Customer(name)

}

Given what we know about the special method name apply, we can now create new customer instances with Customer("Sasha"), omitting the new operator. This is a shorthand for Customer.apply("Sasha").

Many factory methods in Scala programs have been given the name apply so that it’s simple to create instances.

Familiar factories

When programming in Scala, you can hardly avoid using factory methods even if you don’t write any yourself. Indeed, you have already used them, and it’s good to be aware of the fact.

For example, you have created Vector objects by writing Vector(4, 10, 2) and the like without new. That’s short for Vector.apply(4, 10, 2), which calls the apply method on class Vector’s companion object. That apply is a factory method that constructs and returns a vector object. Actually, you can’t even create a vector with new Vector(4, 10, 2), because the vector class doesn’t define such an operation.

Here are some more factory methods that you’ve already encountered:

  • You have written expressions such as Some(newFave), again omitting new as you created a Some object. Adding new would have worked here but is unnecessary.
  • You have used functions such as circle and rectangle to create images; they are methods on the package object o1. Those methods are factory methods that produce Pic objects.
  • Pic("face.png") similarly calls a factory method: it’s shorthand for calling apply on the companion of class Pic.

Whether you use a factory method or new to create instances of a particular class depends on how that class has been defined and which factory methods, if any, are available. In O1, you don’t need to decide when to implement a factory method; to the extent that we’ll use them, they’ll be introduced by the ebook or the Scaladocs.

Why write a factory method?

Perhaps you’re thinking: That’s nice and all, but what is the big deal about factory methods? Wouldn’t it be enough to just create all instances with new as usual?

As far as our Customer example is concerned, fair enough. Now that we wrote the factory method, we can leave out new as we create customer objects, which, some would argue, is enough to justify the method. But the overall benefits are certainly marginal in this case.

So why are factory methods common in the Scala API and elsewhere?

Disappointingly, we haven’t covered enough ground in O1 to discuss the rationale behind factory methods in depth. We can point up the main goal, though. It is flexibility. Compare:

  • By combining new, a class name, and some constructor parameters to create an object, we tangle up two aspects of our program:
    1. what information is needed as a parameter to create the object; and
    2. which class’s instance is created from those parameters.
  • By using a factory method, we can decouple those two aspects instead, so that we can:
    1. Define multiple distinct methods that create objects of the same type from different parameters. For example, the factory methods Pic, circle, and rectangle all produce Pic objects from different inputs.
    2. Create a factory method that doesn’t always return an instance of one specific class but uses its parameter values to select which type of object it creates and returns.

The spring course Programming Studio 2 has more to say about why factory methods make sense.

Summary of Key Points

  • In Scala, any method named apply will work as a “default method” of sorts: the call myObject(params) automatically expands to myObject.apply(params).
    • In effect, an object with an apply method can be used “as though it was a function”.
    • This technique has been used in Scala’s String and Vector classes, among others. They have apply methods that return whichever element is stored at a particular index.
  • Sometimes you need to represent an attribute or operation that isn’t associated with each instance of a class but the class itself. To that end, you can define a so-called companion object for the class.
    • A companion object is often a good place for storing constants associated with the class, for example.
  • A factory method is a method whose purpose is to create and return a new object.
    • Factory methods are used in association with various Scala API classes, instead of directly instantiating those classes with new.
    • In Scala, many factory methods are apply methods on companion objects. This means that you can use expressions of the form ClassName(params) to create instances of those classes; e.g., Vector(432, 32, 223).
  • Links to the glossary: apply; companion object; factory method.

Feedback

Please note that this section must be completed individually. Even if you worked on this chapter with a pair, each of you should submit the form separately.

Credits

Thousands of students have given feedback that has contributed to this ebook’s design. Thank you!

Weeks 1 to 13 of the ebook, including the assignments and weekly bulletins, have been written in Finnish and translated into English by Juha Sorva.

Weeks 14 to 20 are by Otto Seppälä. That part of the ebook isn’t available during the fall term, but we’ll publish it when it’s time.

The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.

The automatic assessment of the assignments has been developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, and Aleksi Vartiainen.

The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.

The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio have done the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.

The other diagrams and interactive presentations in the ebook are by Juha Sorva.

The O1Library software has been developed by Aleksi Lukkarinen and Juha Sorva. Several of its key components are built upon Aleksi’s SMCL library.

The pedagogy behind O1Library’s tools for simple graphical programming (such as Pic) is inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and Krishnamurthi and Picturing Programs by Stephen Bloch.

The course platform A+ has been created by Aalto’s LeTech research group and is largely developed by students. The current lead developer is Jaakko Kantojärvi; many other students of computer science and information networks are also active on the project.

For O1’s current teaching staff, please see Chapter 1.1.

Additional credits appear at the ends of some chapters.

a drop of ink
Posting submission...

Submission received.