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? 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 Modules: None.
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.
- You can obtain an individual element with
myVector(index)
or with the longer expressionmyVector.apply(index)
. Why are there two different-looking but fundamentally similar ways of doing the same thing? - You learned to command objects via method calls, so
myVector.apply(index)
looks legitimate. But isn’t there something odd about the expressionmyVector(index)
? It uses a reference to an object (frommyVector
) and somehow passes the value ofindex
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? - 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 String
s: "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.
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.
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
}
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.
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.Customer
, we increment the companion object’s
createdInstanceCount
variable by one, then copy its new
value in the customer object’s instance variable number
.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:
- companion objects, which share a name with a class defined in the same file; or
- 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.")
}
}
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:
- 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 constantsMinValue
andMaxValue
that represent the edges of the coordinate system (-1.0 and +1.0). - 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.
- 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")
.
An additional example
The StarCoords
companion object has a factory method called
fromPercentages
. Take a look if you feel like it.
Such a method is called a factory method (tehdasmetodi). Its job is simply to generate and return a new object.
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 omittingnew
as you created aSome
object. Addingnew
would have worked here but is unnecessary. - You have used functions such as
circle
andrectangle
to create images; they are methods on the package objecto1
. Those methods are factory methods that producePic
objects. Pic("face.png")
similarly calls a factory method: it’s shorthand for callingapply
on the companion of classPic
.
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:- what information is needed as a parameter to create the object; and
- 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:
- Define multiple distinct methods that create
objects of the same type from different
parameters. For example, the factory methods
Pic
,circle
, andrectangle
all producePic
objects from different inputs. - 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.
- Define multiple distinct methods that create
objects of the same type from different
parameters. For example, the factory methods
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 callmyObject(params)
automatically expands tomyObject.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
andVector
classes, among others. They haveapply
methods that return whichever element is stored at a particular index.
- In effect, an object with an
- 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 formClassName(params)
to create instances of those classes; e.g.,Vector(432, 32, 223)
.
- Factory methods are used in association with
various Scala API classes, instead of directly
instantiating those classes with
- 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!
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, 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 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 was created by Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi, and Paweł Stróżański with input from Juha Sorva, Otto Seppälä, Arto Hellas, and others.
For O1’s current teaching staff, please see Chapter 1.1.
Additional credits appear at the ends of some chapters.
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.