This course has already ended.

The latest instance of the course can be found at: O1: 2024

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.

Kieli vaihtuu A+:n sivujen yläreunan 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? Or maybe an object is like a package? Can I keep my head together?

Topics: apply methods. Companion objects. Some skippable bonus topics.

What Will I Do? Read, mostly.

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

Points Available: B5.

Related Modules: None.

../_images/person03.png

Introduction

In this relatively short chapter, we’ll continue to study just how ubiquitous objects are in Scala programs. We’ll encounter a handful of topics related to this theme.

The Special Method apply

Some example code to begin with

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?

The answers to these questions are intertwined. Let’s start untwining.

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 a parameter list 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 this 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 to print 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 “Treating a Class as an Object”

Let’s turn to another subject. (apply will resurface while we discuss this new topic, though.)

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

end Customer

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

end Customer

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
end Customer

class Customer(val name: String):

  Customer.createdInstanceCount += 1
  val number = Customer.createdInstanceCount

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

end Customer

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 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 module.

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.")

end Customer

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.

Customer("Eugenia Enkeli")res3: Customer = #1 Eugenia Enkeli
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 are often 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.

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 a couple of other uses for companion objects that are more common in practice:

  1. Constants: a companion object is a nice place for storing constants. 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: Sometimes, you’ll want to define a function that’s associated with a class but isn’t a method on the class’s instances. In such cases, you can write the function as a method on the class’s companion object. The companion object will then serve as a convenient place for keeping auxiliary functions that are related to the class. Those functions may be public or — if intended to be used by the class alone — private.

It’s never absolutely necessary to define a companion object, in the sense that one couldn’t make a program work without one. But for the above use cases, companions provide a nice, clean solution. Many classes in the Scala API have a companion object.

Bonus Material

The remaining topics in this chapter aren’t important for O1 or indeed for learning to program in general, but they are occasionally useful when programming in Scala.

Using a singleton object as a package

Scala lets us import methods — and variables, too — from an object. Let’s try it:

object toolkit:
  def sumOfThree(a: Int, b: Int, c: Int) = a + b + c
  val Greeting = "Ave!"// defined object toolkit
import toolkit.*

That imports the contents of the toolkit singleton. With that done, we can access those methods and variables without mentioning the object’s name, as shown below.

sumOfThree(100,10,1)res6: Int = 111
Greetingres7: String = Ave!

Now the sumOfThree call, for example, doesn’t look like we’re calling a singleton object’s method. We invoke sumOfThree as if no target object was involved at all.

Every now and then, you may find it convenient to define such a “package-like” singleton object, which is meant to be imported from and which contains an assortment of methods that are more or less related to each other and .

println, readLine, and "package-like" objects in the Scala API

The familiar println function is actually a method on a singleton object named Predef. This one particular object has an elevated status in Scala: its methods can be called in any Scala program without an import and without an explicit reference to the object.

As for readLine, you learned to use it in Chapter 2.7 by first importing scala.io.StdIn.*. As you did that, you essentially used a singleton object named StdIn as a “package-like object”.

importing just about anywhere

In the ebook’s examples, we have generally placed any import statements at the top of the Scala file. This is a common practice.

Scala also lets you import locally within a particular class or object or even an individual method:

import myPackage1.*

class X:
  import myPackage2.*

  def myMethodA =
    // Here, you can use myPackage1 and myPackage2.

  def myMethodB =
    import myPackage3.*
    // Here, you can use myPackage1, myPackage2, and myPackage3.

end X

class Y:
  // Here, you can use myPackage1 only.
end Y

Such local imports sometimes make code easier to read.

importing from an instance

As noted above, you can use a singleton object like a package and import its methods. As a matter of fact, you can even do the same to an instance of a class, if you’re so minded.

class Human(val name: String):
  val isMortal = true
  def greeting = "Hi, I'm " + this.name// defined class Human
val soccy = Human("Socrates")soccy: Human = Human@1bd0b5e
import soccy.*greetingres8: String = Hi, I'm Socrates

The last command issued above is actually a shorthand for soccy.greeting.

In most cases importing from instances like this is liable to make your code harder to read.

Object creation in Scala and the new keyword

This box contains some further details concerning how instantiation works behind the scenes in Scala. Feel free to skip this box entirely, but you could take a look in case you happen to be interested in this topic or if you happen to know the new keyword from some other language and are wondering about whether you need it in Scala, too.

In some other programming languages, the new keyword is routinely used for creating objects. For example, if we had a class Animal, we’d instantiate it like so:

new Animal("llama")

But in Scala, we’re used to writing this shorter command instead:

Animal("llama")

As it happens, the first command with new also works in Scala, and new is one of Scala’s reserved words. The meaning of the word is to instantiate a class to produce a new object. However, we don’t (almost ever) need to write new in our Scala code. Why not?

Behind the scenes, Scala works as follows. Let’s say you’ve written this code:

class Animal(val species: String)

That defines a class for you, but what it also does, implicitly, is define a companion object for the class and an apply method on that companion object. That companion looks like this:

object Animal:
  def apply(species: String) = new Animal(species)

The generated object has an apply method that corresponds to the class’s constructor parameters. Internally, the apply method instantiates the class using the new keyword.

That auto-generated companion object does not show up in the code you write but exists just the same. If it didn’t exist, we’d need to write new Animal(...) to instantiate class Animal. The companion and its apply method mean that Animal.apply(...) and Animal(...) work equally well. The latter is the simplest and most common way to instantiate a class. (That holds from Scala’s version 3 onwards. In older language versions, it was much more common to type new.) Some more information: Universal Apply Methods.

One further detail: Such an auto-generated apply method has one more word in front of def. That word, inline, isn’t strictly necessary here but it does improve the code’s runtime efficiency. An inlined method doesn’t get allocated a frame on the call stack like other methods do. Instead, any code that calls the method, such as Animal(...), basically gets replaced by the method body, new Animal(...), which happens before the program even runs. For a more general discussion of this topic, see the Wikipedia article for Inline expansion.

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 were 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 class and its companion object can access each others’ private members.

  • Links to the glossary: apply; companion object.

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 and so contributed to this ebook’s design. Thank you!

The ebook’s chapters, programming assignments, and weekly bulletins have been written in Finnish and translated into English by Juha Sorva.

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, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Anna Valldeoriola Cardó, 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 did 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 of using O1Library 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+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services. Markku Riekkinen is the current lead developer; dozens of Aalto students and others have also contributed.

The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It has been designed and implemented by various students in collaboration with O1’s teachers.

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...