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
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.
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?
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 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 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.
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.
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
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:
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.")
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:
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 constantsMinValue
andMaxValue
that represent the edges of the coordinate system (-1.0 and +1.0).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 import
s 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 import
ed
from and which contains an assortment of methods that are more or
less related to each other.
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
import
ing scala.io.StdIn.*
. As you did that, you essentially
used a singleton object named StdIn
as a “package-like object”.
import
ing 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 import
s sometimes make code easier to read.
import
ing 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 import
ing 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 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.
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 other’s 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, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, 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, Juha Sorva, and Jaakko Nakaza. 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; 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.
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.