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 2.4: Inside a Class
Recap
In this chapter, we’ll explore the program code that defines a class. In doing so, we effectively combine what you know about defining a singleton object (Chapter 2.2) with what you just learned about classes and their relationship to objects (Chapter 2.3). Let’s begin with a quick review of some key facts that you’ll need in this chapter.
Things you already know about classes and objects:
Classes define the types of data that a program operates on. A class represents a generic concept that can be instantiated. For example, a class may represent the generic concept of student.
Instantiating a class means creating an object that is an instance of the class. In other words, to instantiate a class means to create an object of the type that the class defines. Such an object is a specific case of the generic concept. For example, the object may represent an individual student.
As you instantiate a class to create an object, you often pass in one or more constructor parameters. The class may use these parameters to initialize instance-specific attributes such as a student object’s name or student ID).
Different objects of the same class each have their own attributes (e.g., student objects may have different names). However, these attributes share the same data type (e.g., each student object’s name object is a string).
Objects of the same type have the same methods. For example, if the student class defines an enrollment method, all student instances will have that method.
Things you already know about singleton objects:
You define a singleton object without separately defining a class for it.
Within a singleton object’s program code, you can define variables that store the object’s attributes. You can also implement methods on the object.
Within a method implementation, the word
this
refers to the object itself. Within an object’s methods, you can use expressions such asthis.someVariable
to access the object’s own variables.
Class Definition vs. Singleton Definition
In Chapter 2.2, we implemented and used a singleton object that represents an individual
employee. In contrast, in Chapter 2.3 we used a class Employee
that represents
employees more generally and was therefore more useful than the original singleton.
The code of the singleton object from Chapter 2.2 is reproduced below. Below it, you’ll
find a previously unfamiliar piece of code that defines the Employee
class. Comparing
the two, it’s easy to see that they are in many respects identical. There are a few key
differences, however, as highlighted by the green boxes between the two code fragments.
object employee:
var name = "Edelweiss Fume"
val yearOfBirth = 1965
var monthlySalary = 5000.0
var workingTime = 1.0
def ageInYear(year: Int) = year - this.yearOfBirth
def monthlyCost(multiplier: Double) = this.monthlySalary * this.workingTime * multiplier
def raiseSalary(multiplier: Double) =
this.monthlySalary = this.monthlySalary * multiplier
def description =
this.name + " (b. " + this.yearOfBirth + "), salary " + this.workingTime + " * " + this.monthlySalary + " e/month"
end employee
After the class name, we define the constructor parameters that need to be provided when instantiating the class. A singleton object doesn’t have this bit.
We can use the parameter variables to initialize the object.
Here, we store the parameter values in the attributes of the
new Employee
instance. (More on that below.)
The methods of our singleton and class are exactly the same!
class Employee(nameParameter: String, yearParameter: Int, salaryParameter: Double):
var name = nameParameter
val yearOfBirth = yearParameter
var monthlySalary = salaryParameter
var workingTime = 1.0
def ageInYear(year: Int) = year - this.yearOfBirth
def monthlyCost(multiplier: Double) = this.monthlySalary * this.workingTime * multiplier
def raiseSalary(multiplier: Double) =
this.monthlySalary = this.monthlySalary * multiplier
def description =
this.name + " (b. " + this.yearOfBirth + "), salary " + this.workingTime + " * " + this.monthlySalary + " e/month"
end Employee
Now, let’s dig into how the class works.
Instance Variables
How does a class definition determine what happens when we create a new instance?
The answer is illustrated in the following animation. The animation also introduces the key concept of instance variable (ilmentymämuuttuja).
You can also consider the instantiation process in terms of the form metaphor that we used earlier. Here’s an update to the illustration from Chapter 2.3:
Instantiation and a class’s program code (a clarification)
It came up above that the commands inside a class definition run when a new instance is created. For example, the instance variables of a new employee object are assigned (initial) values at that point.
Note that this applies only to the commands that appeared directly within the class definition, not the commands located inside methods. A method’s body is executed only when we instruct the computer to do so by calling the method.
For example, when initializing an instance of class Employee
, none of the methods
ageInYear
, monthlyCost
, raiseSalary
, or description
are executed, even though
these methods are defined within the class’s program code. The class’s user can call
those methods after creating an instance, as we’ve done in earlier examples.
Check your understanding: constructor parameters and instance variables
Constructor parameters vs. instance variables
A method’s parameter variables exist — and their values are held in memory — only as long as the method is being executed. Similarly, constructor parameters exist only while the new instance is being created. They are stored in a frame on the call stack; that memory is released for other use as soon as the object has been initialized.
Instance variables, on the other hand, are members of an object. Their values remain in memory after the object’s been created and persist between method calls.
An instruction such as val name = nameParameter
essentially takes a value received as a
parameter and stores it in a place where it can be accessed later.
In classes Employee
and Pet
, the only thing we did with the constructor parameters
is copy their values into instance variables. You may reasonably be wondering why we even
need a separate set of variables to store the constructor parameters, if those parameters
are there just to be copied into another set of variables.
It does make sense to be able to define constructor parameters as separate from instance variables, because copying their values isn’t the only thing we can do with the parameters; you’ll see examples of that later. But since it’s common to do the sort of copying that our example classes do, it should be possible to express these classes more succinctly. And indeed Scala does provide a way to tighten our code:
A Convenient Way to Write a Class
Below are two versions of class Employee
. The first version you saw already, the second
is more compact. Both accomplish the same thing.
class Employee(nameParameter: String, yearParameter: Int, salaryParameter: Double):
var name = nameParameter
val yearOfBirth = yearParameter
var monthlySalary = salaryParameter
var workingTime = 1.0
// methods not shown
end Employee
We can write var
or val
at the top of the class definition,
as shown below. What this means is that we both require the
class’s user to pass in these three things as constructor
parameters and store the received values in the new object’s
instance variables.
In the more verbose version above, we had to name the parameter variables separately. Those names don’t feature at all in the compact version of the class. In the shorter code, the constructor parameters have the same names as the instance variables; their values are copied into the instance variables without an explicit command to do so.
Our example class has a fourth instance variable, which doesn’t receive its value from a constructor parameter. Its definition is identical in both versions.
class Employee(var name: String, val yearOfBirth: Int, var monthlySalary: Double):
var workingTime = 1.0
// methods not shown
end Employee
If you want, you can view the following animation, which uses the compact version of
Employee
.
The compact version is shorter and, perhaps, easier to read. In any case, the compact style is extremely common in Scala programs and you should get accustomed to it. In O1, too, we’ll adopt this compact style of writing classes.
Methods on Instances
Methods operate on a class’s instances in essentially the same way as they operate on
singleton objects. As you saw in Chapter 2.2, we can use the word this
to refer to
the object whose method has been called. A class’s instance, too, can access its own
instance variables through expressions of the form this.variableName
.
If this seems perfectly clear to you, feel free to skip the following animation; it has no other new content. Otherwise, view the animation.
Assignment: A Compact Rectangle
Assume we have access to a class Rectangle
, which we can use as follows:
val test = Rectangle(10.0, 15.5)test: o1.Rectangle = o1.Rectangle@152a308 test.side1res0: Double = 10.0 test.side2res1: Double = 15.5 test.areares2: Double = 155.0
As shown, we can ask a Rectangle
object for the lengths of its sides as well as its
area.
Here’s one way to define a class that works as shown above:
class Rectangle(givenSideLength: Double, anotherGivenSideLength: Double):
val side1 = givenSideLength
val side2 = anotherGivenSideLength
def area = this.side1 * this.side2
// Etc. You may write additional methods here.
end Rectangle
This definition is unnecessarily wordy. Write a more compact class definition by defining
the instance variables in the class header, as demonstrated for class Employee
above.
Write your solution in Rectangle.scala
in the IntroOOP module. The given file contains
the above implementation for you to modify.
Use the REPL to confirm that your new implementation works as illustrated.
A+ presents the exercise submission form here.
Mini-Assignment: Describing a Rectangle
Assignment: An Account Class
Further practice: a Pic
-returning method
As noted, the class Rectangle
represents certain mathematical attributes
of rectangles. If we want to, it’s possible to extend the class with
methods that operate on rectangles as graphical elements.
Add a method makePic
in class Rectangle
. The method should:
take in a single parameter of type
Color
; andreturn an image (
Pic
) of a rectangle whose width is equal toside1
, whose height is equal toside2
, and whose color is determined bymakePic
’s parameter.
A+ presents the exercise submission form here.
In the practice task just above, we considered the drawing color to be an additional piece of information that’s passed to the rectangle object whenever we wish it to produce an illustration of itself.
Alternatively, we may choose to represent color as an inherent attribute of each rectangle.
Stay in Rectangle.scala
and add another class there, ColoredRectangle
.
As a starting point, you can copy and paste the definition of Rectangle
and rename it. Then make the following changes to the new class:
Add a third constructor parameter, of type
Color
.See to it that the rectangle’s color is accessible through an instance variable called
color
.Edit
makePic
so that it takes no parameters and instead uses the rectangle object’s color attribute as it produces an image of the rectangle.
A+ presents the exercise submission form here.
Which of the two classes is better? As far as our toy example is concerned, there is little difference, and in any case the answer depends on what we might wish to use the class for.
Here is a more interesting general question that you may want to consider: if you have a class that represents an abstract concept (such as rectangle), should its definition include anything that pertains to the program’s outward appearance (such as colors and graphics)? We’ll return to that question later on.
A Monster Example
Let’s define a class that could represent monsters in a simple, otherwise imaginary game.
Monsters have a kind (e.g., "orc", "vampire") and a health score. We mean to use our monster class as illustrated in the example below.
val creature = Monster("dragon", 200)creature: Monster = Monster@597f1753 creature.kindres3: String = dragon creature.healthMaxres4: Int = 200 creature.healthNowres5: Int = 200 creature.descriptionres6: String = dragon (200/200)
We create an instance of the class, a monster object. The constructor parameters specify the type of this monster and its health when it’s at full strength.
We can ask any monster object to tell us its kind as well as its maximum health and current health. A monster starts at full health, so the two numbers are identical for now.
We can also ask the monster for a string that describes the monster. This description includes information about the monster’s current and maximum health.
A monster’s state changes when it suffers damage (having been attacked by a hero or something):
creature.sufferDamage(30)creature.healthNowres7: Int = 170 creature.sufferDamage(40)creature.healthNowres8: Int = 130 creature.descriptionres9: String = dragon (130/200)
We pass a parameter to the sufferDamage
method to indicate how
many health points the monster loses. This effectful method only
adjusts the monster’s state and does not return anything.
We can observe the change by asking the monster for its updated health or description.
Now let’s see about implementing such a class:
The term “constructor”
This chapter has said plenty about constructor parameters. That term, which refers to the parameters used when instantiating a class, comes from the concept of a constructor (konstruktori). A constructor is a subprogram that’s executed as soon as an object is created; its purpose is to initialize the object. Commands that assign values to instance variables are common in a constructor.
In Scala, the commands we write directly into a class definition, outside of its methods, serve as a constructor for the class. In many other programming languages, a constructor has a separate definition within a class much like methods do.
Assignment: Odds (Part 1 of 9)
There are different ways to express the likelihood, or odds, of an event happening. For example, to describe the odds of rolling a six on a six-sided die, we might say that the odds are “one out of six”. Another way to phrase this is “5/1”, which means that there are five ways to not roll a six and one way to roll it. In other words: it’s five times as likely to not roll a six as it is to roll it. The event’s likelihood may also be expressed as the percentage 16.67%, which can be alternatively written as 0.1667.
Let’s create a class for expressing the odds of events in different formats, and, eventually, computing the probabilities of combinations of events. In this chapter, we’ll begin our work on this class; in future chapters, we’ll return to improve on what we accomplish here.
Introduction: what we want from our Odds
class
The basic idea is that one instance of class Odds
stands for the likelihood of a single
event such as rolling a six or a bookmaker’s estimated odds of Norway winning the
next Eurovision Song Contest. We intend to create Odds
objects like this:
val rollingSix = Odds(5, 1)rollingSix: Odds = o1.odds.Odds@1c60524
Now the variable rollingSix
stores a reference to an object of type Odds
. As
constructor parameters, we passed in first the number of non-occurrences (5) followed by
the number of occurrences (1); together, these two numbers specify the event’s odds.
We can now ask the object to describe these odds in a variety of formats. Here’s
the probability
of rolling a six as a Double
(i.e., the value of 1 ∕ (5 + 1)):
rollingSix.probabilityres10: Double = 0.16666666666666666
The fractional
method describes the odds as a relation between non-occurrences and
occurrences:
rollingSix.fractionalres11: String = 5/1
As you can see, the method returns a string that connects the two constructor parameters with a slash.
The decimal
method returns the reciprocal of what probability
returns. That is,
the method’s return value describes the odds in “one-in-how-many” terms. Our example
event has a one-in-six chance of happening:
rollingSix.decimalres12: Double = 6.0
Consider another example. Many international betting agencies report the odds on offer in
fractional format (as returned by fractional
). We can use an Odds
object to represent
offered bets. For example, suppose that odds of 5/2 are on offer for the event of Norway
winning Eurovision; we’ll model this as an Odds
object:
val norwayWin = Odds(5, 2)norwayWin: Odds = o1.odds.Odds@1e75d66 norwayWin.probabilityres13: Double = 0.2857142857142857 norwayWin.fractionalres14: String = 5/2 norwayWin.decimalres15: Double = 3.5
In a betting context, decimal
’s return value is the number that a bettor’s investment
multiplies by. For example, if the odds are 5/2, the successful bettor will receive 3.5
times what they bet: their money back and 2.5 times that much extra.
With that in mind, let’s sketch out one more method for our class. In this example, we
compute the winnings
of a bettor whose investment of 20 currency units on Norway has
paid off:
norwayWin.winnings(20.0)res16: Double = 70.0
(The result is the bet of 20 multiplied by a factor of 3.5, which factor is returned by
decimal
.)
Finally, here’s one more example. This example demonstrates that it’s perfectly possible for the first constructor parameter to be smaller than the second, in case the event is likely:
val threeOutOfFiveChance = Odds(2, 3)threeOutOfFiveChance: Odds = o1.odds.Odds@dfdd0c threeOutOfFiveChance.probabilityres17: Double = 0.6 threeOutOfFiveChance.fractionalres18: String = 2/3 threeOutOfFiveChance.decimalres19: Double = 1.6666666666666667 threeOutOfFiveChance.winnings(123.45)res20: Double = 205.75
Your assigment
Fetch the Odds module and locate the definition of class Odds
therein. (For now, pay
no mind to the module’s other contents.)
You’ll find the class lacking in functionality: probability
is there, but fractional
,
decimal
, and winnings
are missing. Implement those methods so that they work as
illustrated above.
Instructions and hints
Two instance variables have already been defined for you:
wont
andwill
. They receive their values directly from constructor parameters (in the manner described above at A Convenient Way to Write a Class).You can use these instance variables in the methods you write.
You won’t need other instance variables.
There’s no need to change
probability
; it works. But you can take a look at how it works.As long as you make sure that you understand what each method is supposed to accomplish, actually implementing the methods with a combination of basic arithmetic operations shouldn’t be a lot of work.
Use the REPL to test that your methods work as specified.
Make sure to select the Odds module as you launch the REPL.
Remember to reset the REPL after making changes.
Submit your program only when you’re satisfied with how it works.
When you implement
fractional
, don’t try to reduce the fraction. If the constructor parameters are, say, 6 and 2, return6/2
, not3/1
.If you run into trouble constructing the string, return to check the
description
method in the rectangle assignment.
Can you implement
decimal
andwinnings
by calling another method of the same class in the method body? (This isn’t required, but it is convenient.)
A+ presents the exercise submission form here.
Should I Have a Method or a Variable?
A couple of excellent questions from students:
When should I write a def
and when a val
? I’m not quite
grasping it.
In the Odds task, why are probability
, fractional
, and decimal
supposed to be methods? Since the methods don’t take any parameters,
wouldn’t this program work just as well if we turned them into
instance variables? I mean, it does work, I tried it. So why have
them as methods?
Consider, for instance, the outline of Odds
below. We could replace
the first three def
s with val
s, and the code would still work.
class Odds(…):
def probability = …
def fractional = …
def decimal = …
def winnings(bet: Double) = …
end Odds
If probability
, fractional
, and decimal
are def
ined as methods like so, each
of those methods’ program code is run only when — or if — that method is actually
called on an Odds
object. On the other hand, the computations written into the method
body will be carried out each time the method is called, which means that calling the
same method multiple times will make the computer compute the same value multiple times.
Had we used val
instead, our Odds
object would have had three additional instance
variables. Giving initial values to those instance variables would be part of an Odds
object’s initialization (its constructor), and the corresponding computations would be
performed as soon as each Odds
object is created. The results of those computations
would then remain stored in memory as part of the object. Under this scenario, when we
“ask” the object to provide that information, it simply accesses the stored values. The
computer wouldn’t have to recompute, say, probability
multiple times, even if we access
it repeatedly. On the other hand, the computer would always compute each of these three
values, once, for every single Odds
object that we create (even though we might not have
a use for all three values on each object). Each Odds
object would also take up some
more space in the computer’s memory, since the objects would have more instance variables.
Such decisions thus affect what gets stored in memory and what the computer recomputes on request. In other words, these decisions have an impact on how much memory a program needs and how fast it runs.
How important that impact is depends on the methods/variables in question and on what
the program does with them. In our tiny example, the decision is inconsequential, because we
create only a handful of Odds
objects and invoke their methods only a handful of times,
and because each Odds
object takes up only a minuscule amount of memory in either case.
However, memory use and efficiency are not the only factors to consider when choosing
between a method and a variable. Often it is absolutely crucial whether something is
stored in memory (val
or var
) or recomputed on request (def
); this may decisively
affect whether the program works right at all. For example, recall the Monster
class
above, which didn’t work until we turned its description
variable into a method that
generates a report from the monster’s current state, whenever the method is called.
(And here’s a hint: in a near-future chapter, you’ll encounter the reverse situation:
where you must use a variable rather than a method.)
Oh, and here’s a simple-looking but significant question to think about
When you solved the programming assignment above, you probably
stored Odds
’s two constructor parameters in val
s, no?
(Hope so!)
Now consider the student questions about replacing def
s with
val
s in Odds
. In what way would things be different if you had
the constructor parameters in var
s rather than val
s?
Give it a think. I’ll put the answer in weekly bulletin 3.0, which will be published after the Week 2 deadline.
A Singleton Object as an Extension of a Class
To conclude this chapter, let’s fool about with a silly little class. As we do so, we’ll pick up a few tricks that’ll come in handy in the next chapters.
class Person(val name: String):
def say(sentence: String) = this.name + ": " + sentence
def reactToSriracha = this.say("What a nice sauce.")
def reactToKryptonite = this.say("What an odd mineral.")
end Person
Each person has a method that commands the person to say something. The method returns a string that contains the person’s name and the sentence they utter.
A person can also be commanded to react to certain things. The person reacts by saying stuff, and therefore...
... it’s convenient to implement these methods by calling the
person’s own say
method. You can think of this as the person
object sending itself a message: “When commanded to react to
something, I’ll command this
— which is me — to execute
the say
method with a specific parameter value.”
An example of using the class:
val first = Person("Jimmy")first: Person = Person@5cf6635a first.say("Super-Duper!")res21: String = Jimmy: Super-Duper! first.reactToSrirachares22: String = Jimmy: What a nice sauce. first.reactToKryptoniteres23: String = Jimmy: What an odd mineral. val second = Person("Lois")second: Person = Person@645b797d second.reactToKryptoniteres24: String = Lois: What an odd mineral.
Every Person
likes sriracha and is puzzled by kryptonite, since that’s what the class
says people should do. That’s pretty much all a Person
can do; they can’t fly, for
example. But not everyone is bound by the same laws:
Adding an instance-specific method
object superman extends Person("Clark"): def fly = "WOOSH!" end superman// defined object superman
We define a singleton object that is a special case of the
person class. The extends
keyword enables us to do this: we
“extend” the definition of the class for the purposes of this
particular object.
The Person
class requires us to specify a constructor parameter
(person name) for any object that we create. For the special
Person
that we’re creating here, we pass the constructor
parameter as part of the extends
definition.
We add a method to this one person alone. Other instances of
Person
do not have this method.
The end marker is again optional, and it would be fine to omit it from such a short and simple object definition.
Superman can now fly in addition to being able to state his name and react to things like a normal person. This object, too, is a person object.
superman.flyres25: String = WOOSH! superman.nameres26: String = Clark superman.reactToKryptoniteres27: String = Clark: What an odd mineral.
We managed to add a tailor-made method to a specific object. What about adapting a class’s behavior?
Overriding a method
Green kryptonite is Superman’s weakness. If we wish to represent Superman as a special
case of class Person
, can we still account for this fact?
What we need is a way to make a particular person’s behavior deviate from the way the other person objects behave.
object realisticSuperman extends Person("Clark"): def fly = "WOOSH!" override def reactToKryptonite = "GARRRRGH!"// defined object realisticSuperman realisticSuperman.reactToKryptoniteres28: String = GARRRRGH!
We redefine a method already defined in class Person
,
for this one person alone.
The redefinition overrides (korvata) the definition in the
class. In order to prevent us from accidentally doing something
like this, Scala sensibly requires us to state that we actually
want to override an existing definition. (Had we omitted the
override
keyword, we would have received an error message.)
Summary of Key Points
A class’s program code defines the instance variables and methods that the class’s instances (objects) have.
When a class is instantiated, the new object gets its own copies of the instance variables, with values specific to that object.
A class also defines which constructor parameters you must provide when you create a new instance. It’s common to store the values of constructor parameters in the new object’s instance variables.
It’s possible, and occasionally useful, to attach additional methods to a specific instance of a class. It’s also possible to override an existing method definition with an instance-specific implementation.
Links to the glossary: class, instance, instance variable, to instantiate, constructor parameter;
this
; to override.
This is important stuff
Instance variables and their relationship to constructor parameters are among the trickier concepts in introductory object-oriented programming. Since they are absolutely crucial to class-based OOP and the rest of O1, we have devoted quite a few words to them in this chapter.
We strongly suggest that you try to make sure you understand these concepts before advancing to the chapters that follow. Review the examples and animations above as needed and chat with the teaching assistants and your fellow students.
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.
The definition of a singleton object begins with
object
. A class definition begins withclass
. You may optionally define anend
marker on either one, and, for clarity, it’s often a good idea to do so.