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. Myös suomenkielisessä materiaalissa käytetään ohjelmien koodissa englanninkielisiä nimiä kurssin alkupään johdantoesimerkkejä lukuunottamatta.
Voit vaihtaa kieltä A+:n valikon yläreunassa olevasta painikkeesta. Tai tästä: Vaihda suomeksi.
Chapter 2.6: Many Ways to Use a Variable
About This Page
Questions Answered: Variables are useful for a bunch of different things, aren’t they? How can I represent, say, the states of a game as a model of interconnected objects? How can I refer from one class that I wrote to another?
Topics: Variables. The roles of variables: constants and other
fixed values, temporary variables, gatherers, most-recent holders.
Storing references in instance variables; using classes as types of
instance variables. A few details about Scala: empty parameter
lists, the package
keyword.
What Will I Do? Read, first. Then, we’ll get started on a game project.
Rough Estimate of Workload:? A couple of hours. The first half should be a breeze. The second half isn’t too difficult either, assuming an understanding of the preceding chapters.
Points Available: A55.
Variables Grouped in Different Ways
Chapter 1.4 told us: in a computer program, a variable is a named storage location for a single value.
That applies to all the variables that you’ve encountered, but those variables differ from each other in a number of other respects. Let’s pause for a moment to sort out what we already know.
We can group variables in categories using a variety of criteria.
Grouping by mutability
Scala explicitly divides variables in val
s and var
s (Chapter 1.4). This is
depicted as a diagram below.
(Many other programming languages don’t make this distinction, or at least don’t emphasize it in the way Scala does.)
Grouping by data type
Another fairly obvious thing to do is to classify variables by their data type:
Grouping by context of use
A third categorization is based on the fact that we can define variables in different contexts.
Some variables are instance variables defined on objects (Chapter 2.4). Others are local variables of a subprogram; they exist in a frame on the call stack only while the computer runs the subprogram. Parameter variables are a special kind of local variable: they aren’t assigned a value by direct command but receive values from parameter expressions in a subprogram call (Chapter 1.7). The variables that we define in the REPL can also be considered as local variables whose lifetime spans the REPL session.
Roles of Variables
We can also group variables by the way we use them, their roles. Although we haven’t paid any particular attention to the fact so far, you have already seen variables being used in a few different roles.
In Chapter 2.2, our account object had a couple of variables that were used in different
ways. The account’s number
variable never changed its value. On the other hand,
balance
’s value changed whenever we deposited or withdrew money; more specifically,
we used the variable’s old value and the size of the adjustment to determine the variable’s
new value. Clearly, this variable had a different part to play in the program than the
account number.
The role of a variable (muuttujan rooli) characterizes how you use the variable in a program: on what grounds do you change its value, if indeed you do change it? Research suggests that it’s possible to describe most variables in computer programs aptly with a dozen or so role labels. Eight role names are enough to label nearly all the variables in O1’s example programs.
For example, we can say that the account’s balance
variable has a role of “gatherer”:
at any given time, its current value has been obtained by gathering and combining the
effects of earlier operations — in this case, by summing depositions and withdrawals.
In this ebook, we’ll use roles of variables as an aid for designing programs. Many of the example programs have variables annotated with role labels as shown below:
var balance = 0.0 // gatherer
A variable’s role doesn’t express everything that one can do with the variable. If we
had wanted to, we could perfectly well have assigned any number we pleased to balance
.
The role describes how the variable is actually used in the program. It has significance
to the human programmer, not to the computer.
There are a few roles that you’ve already seen a proper example of. Let’s discuss each one in turn.
Fixed values
The simplest role for a variable is the fixed value (kiintoarvo). Once a
fixed-valued variable has been initialized, it’s never changed. In Scala, fixed
values are practically always val
s.
A fixed-valued instance variable describes a permanent attribute of an object (such as the account number).
As for fixed-valued local variables, parameters are the most common example. For example,
the multiplier
parameter of method monthlyCost
in class Employee
is a fixed value
whose value remains unchanged thoughout the entire method call:
def monthlyCost(multiplier: Double) = this.monthlySalary * this.workingTime * multiplier
Constants
Fixed-valued variables whose value is already known before the program run are often called constants (vakio). A programmer may define a constant to represent a universal fact such as an approximate value of π, or an application-specific fact that is known in advance.
val Pi = 3.141592653589793
val MinimumAge = 18
val DefaultGreeting = "Hello!"
Constants are a good tool for making programs easier to read. A constant’s name communicates the programmer’s intent better than a “magic number” — which is programmer-speak for a literal with an undocumented purpose.
Code that uses constants can be easier to modify, too. If we want to exchange a constant value for another as our program evolves, we can do that with a single change to the constant definition, even if the value is used in various places throughout our program (or even across multiple programs). Magic numbers, in contrast, create implicit dependencies between different parts of code: if we change one number, we may easily forget to make the corresponding changes elsewhere. Such implicit coupling frequently results in bugs.
Many software libraries define constants. For example, the package scala.math
provides
a fixed-valued variable Pi
that stores an approximation of π. The colors that you’ve
used from package o1
(such as Red
, Blue
, and CornflowerBlue
) are also constants,
each of which refers to a single object of type Color
.
Temporaries
In Chapters 1.8 and 2.2, you already created temporaries (tilapäissäilö). These
variables do a “temp job” of storing a value for later use by the algorithm in which they
appear. For example, in the account’s withdraw
method, you needed to store the withdrawn
sum temporarily, while the balance was being adjusted, before returning the temporary’s value:
def withdraw(sum: Int) = {
val withdrawn = min(sum, this.balance) // withdrawn is a temporary
this.balance = this.balance - withdrawn
withdrawn
}
A similar use for a temporary is to store an intermediate result in a method that performs a sequence of arithmetic operations. Temporaries are typically local variables.
The value of most temporaries never changes once set; in Scala programs, almost all
temporaries are val
s. Whether you describe a variable as a fixed value or a temporary
is a matter of taste.
Gatherers
When we assign a new value to a gatherer (kokooja), we determine the new value by somehow combining the gatherer’s old value with new data such as user input or some other parameter.
Here are a couple of examples of instance variables that serve as gatherers:
- The balance of an account (already discussed above). The command
this.balance = this.balance - withdrawn
is typical of a gatherer: it computes the new balance from the old balance and another value. - A monster’s current health (in Chapter 2.4), which worked much like an account’s balance.
- The location of a playable character in a game that determines
the character’s current location based on 1) where the character was
previously; and 2) the direction the player instructed the character
to move in.
- The code might look something like this:
this.location = this.location.neighbor(directionOfMove)
. (The character object is associated with a location object. It asks the location to determine the neighboring location in a particular direction and sets that other location as its current location.) - We’ll do something similar later in this chapter.
- The code might look something like this:
You can think of a gatherer as a variable that receives additional pieces of information so that its value at any given time depends on each piece of information that it received previously.
Gatherers are common not just as instance variables but as local variables, too. We’ll come across a local gatherer for the first time in Chapter 5.5.
Most-recent holders
We can assign a new value for the name
of an Employee
:
val testEmployee = new Employee("Issur Danielovitch", 1916, 12345)testEmployee: o1.Employee = o1.Employee@1100b25 testEmployee.nameres0: String = Issur Danielovitch testEmployee.name = "Kirk Douglas"testEmployee.name: String = Kirk Douglas
The variable name
keeps track of the latest name that’s been assigned to the employee.
The latest name simply replaces the earlier value; the old value is not used when
determining the new one, as was the case with gatherers. We say that a variable such as
name
, which stores the latest value of a particular kind, is a most-recent holder
(tuoreimman säilyttäjä).
name
is typical example of a most-recent-holding instance variable: it stores an object
attribute whose value can be exchanged for another. Most-recent holders can be useful as
local variables, too, which we’ll explore in Chapter 5.5.
Benefiting from roles
Role names characterize the things we programmers typically use variables for. We can use them as tools for thinking about the programs that we write and read. If you know a variable’s role, you also know something about the behavior of the program you’re working on.
Each role corresponds to the abstract solution of a small subproblem that occurs time and time again in diverse programming problems. The role labels capture common patterns of variable use that experienced programmers perceive in otherwise unrelated real-world programs.
On roles and design patterns
The roles of variables are one way to label solution patterns for common programming needs. Roles describe individual variables, which are very small components of an entire program. Programmers have also come up with descriptions of larger-scale patterns; the best-known work in this vein is known as design patterns (suunnittelumalli). Each design pattern captures a recurring problem in program design and suggests a solution to it on a general level, typically at a granularity of one or more classes. Design patterns will be discussed further in CS-C2120 Programming Studio 2.
Unlike some design patterns, role names aren’t part of most professional programmers’ vocabulary. Those professionals who are familiar with the concept may benefit from roles as they document their code, for instance. Perhaps the roles of variables will be slightly better known by the next generation of programmers?
You, as an O1 student, aren’t required to use roles to label your variables. However, you may find them helpful as you sketch out solutions to programming problems; many beginner programmers have. One of the challenges of learning to program is recognizing recurring subproblems in apparently different problems so that you can apply a known solution. This is where the roles of variables can help you. Roles hint at how you can solve certain subproblems that you’ll repeatedly run into as you program.
Use roles as a tool for thinking:
“Hmm... I’m supposed to produce the sum of all the scientific measurements that I receive as inputs... I could use a gatherer to keep track of the sum as it accumulates. Every time I process a new measurement, I’ll add the new measurement to the gatherer’s old value.”
When you write programs, consider each variable’s data type and role. When you read programs, notice how variables are used in a number of roles. When you document a program, you may be able to assist the reader by annotating roles as comments in code.
Interconnected Objects
Our earlier examples have shown that you can think of an object-oriented program’s
behavior
as communication between objects. For that communication to work, we need to
connect objects to each other. A course object, for example, might refer to a room object
that represents the classroom where the course is taught as well as to a number of other
objects that represent the enrolled students. Similarly, in the GoodStuff application, a
Category
object is linked to the experiences in that category, one of which is the
diarist’s favorite. An object that represents a character in a game might store a
reference to a Pos
object that represents the character’s current location.
In earlier chapters, you have learned to define a type as a class. What we haven’t done yet is write a class that refers to another custom class that we wrote. Which is what we’ll do now.
As a first step, we’ll examine an example whose two classes represent — in greatly simplified fashion! — the orders and customers of an imaginary online store. After that, you’ll get to practice what you learned by defining an object-oriented model for a game.
Our goal: classes to represent customers and orders
When we create a customer object, we provide a name, a customer number, an email address, and a street address as constructor parameters:
val testCustomer = new Customer("T. Tester", 12345, "test@test.fi", "Testitie 1, 00100 Testaamo, Finland")testCustomer: o1.Customer = o1.Customer@a7de1d
Calling description
gives us a textual summary of key information:
println(testCustomer.description)#12345 T. Tester <test@test.fi>
To create an order, we specify an order ID number and a customer. The latter parameter is
a reference to a Customer
object:
val exampleOrder = new Order(10001, testCustomer)exampleOrder: o1.Order = o1.Order@18c6974
The above creates an empty order with no items yet, but we can call addProduct
to add
them. In this simple example, we don’t actually concern ourselves with any product details.
Instead, we just indicate the product’s price per unit and the number of units that are
being bought. In the REPL session below, we place an order for 100 items priced at 10.5
euros each plus a single 500-euro product.
exampleOrder.addProduct(10.5, 100)exampleOrder.addProduct(500.0, 1)
Order
objects, too, have a description
:
println(exampleOrder.description)order 10001, ordered by #12345 T. Tester <test@test.fi>, total 1550.0 euro
As shown, the ordering customer’s description is part of the order’s description.
We can also ask an order object to tell us who placed the order, producing a reference to that customer object:
exampleOrder.ordererres1: Customer = o1.Customer@a7de1d
Crucially, what we got is a reference of type Customer
; we didn’t get a string, for
example. This means that we managed to use the order object to access another object
associated with it. We can use that other object just like any other, as in this chain
of requests:
exampleOrder.orderer.addressres2: String = Testitie 1, 00100 Testaamo, Finland
What happens here is that the variable exampleOrder
contains a reference to an order
object, that order object’s orderer
variable contains a reference to a customer object,
and the customer object’s address
variable contains a reference to a string.
Implementing the two classes
Here is the customer class. There isn’t really anything new about it:
class Customer(val name: String, val customerNumber: Int, val email: String, val address: String) {
def description = "#" + this.customerNumber + " " + this.name + " <" + this.email + ">"
}
Now to the other class. Let’s first sketch it out as pseudocode (Chapter 2.5):
class Order(fixed values: a number and a customer who placed the order) { let’s use a gatherer to keep track of the total price, which starts at zero def addProduct(pricePerUnit: Double, numberOfUnits: Int) = { multiply the parameters and add the result to the gatherer } def description = return a string description of the order, requesting the customer info from the customer object associated with this order }
This pseudocode translates easily to actual program code, since it’s perfectly okay to
use the other class we wrote, Customer
, in our definition:
class Order(val number: Int, val orderer: Customer) {
var totalPrice = 0.0
def addProduct(pricePerUnit: Double, numberOfUnits: Int) = {
this.totalPrice = this.totalPrice + pricePerUnit * numberOfUnits
}
def description = "order " + this.number + ", " +
"ordered by " + this.orderer.description + ", " +
"total " + this.totalPrice + " euro"
}
Customer
as the type of one of Order
’s
constructor parameters. This means that when creating an Order
instance, we need to provide a reference to a Customer
instance. We add the word val
so that the reference also
gets stored in an instance variable.this.orderer
evaluates to a reference that points to a customer
object. We can call its method by writing this.orderer.description
:
the order object commands the customer object to produce its
description, then uses the string it receives as part of its
own description.That’s all it took to define a link from class Order
to class Customer
and, by
extension, from each order object to one customer object. These relationships between
objects are depicted in the diagram below.
Assignment: more toString
methods
As an optional mini-assignment, modify the given classes Customer
and Order
so that instead of a description
method, they have a
toString
-method (Chapter 2.5). You’ll find the classes in
the IntroOOP
module.
Notice that once toString
is defined on customers, you don’t need
to explicitly invoke it in the toString
of class Order
. It
suffices to concatenate a string with a reference to a customer
object.
If you want more practice, try modifying the description
method
of Order
s to use string interpolation (s
and dollar signs)
instead of plus operators.
A+ presents the exercise submission form here.
How about linking to many objects?
What if we want each course object to store references to multiple enrolled students or each category to refer to multiple experiences? Or, say, record in each customer object a list of all the orders that customer has placed?
It’ll take until Chapter 4.2 before we tackle this question in earnest. Until then, here’s the short of it: we store the students, experiences, or orders as the elements of a collection and link that collection to the course, category, or customer object.
More Examples
Example: Guarded treasures
In Chapter 2.4, we worked on a Monster
class. Here’s a working version (with
description
replaced by a toString
method):
class Monster(val kind: String, val healthMax: Int) {
var healthNow = healthMax
override def toString = this.kind + " (" + this.healthNow + "/" + this.healthMax + ")"
def sufferDamage(healthLost: Int) = {
this.healthNow = this.healthNow - healthLost
}
}
We’re not going to build a full-build game around this particular example, but let’s extend the theme by adding another toy class.
Let’s say our imaginary game features hoards of treasure, each guarded by a troll —
each Treasure
object is associated with a Monster
object that is the treasure’s
guardian. Here’s a usage example:
val hoard = new Treasure(1000.0, 50)hoard: Treasure = Treasure (worth 1000.0) guarded by troll (50/50) hoard.valueres3: Double = 1000.0 hoard.guardianres4: Monster = troll (50/50) hoard.appealres5: Double = 20.0
Double
.
Moreover, each treasure has a challenge level, which
is also a number.Treasure
object to provide its value
and its guardian.Treasure
is further characterized by its appeal
:
the treasure’s value divided by its guardian’s current
health score. That is, a treasure’s appeal will go up
as its guardian’s health goes down. In this example,
we have a treasure guarded by 50-health troll at full
strength, so the treasure’s appeal is 1000.0/50 or 20.0.Here’s an implementation for class Treasure
.
class Treasure(val value: Double, val challenge: Int) {
def guardian = new Monster("troll", this.challenge)
def appeal = this.value / this.guardian.healthNow
override def toString = "treasure (worth " + this.value + ") guarded by " + this.guardian
}
Example: Bosses of bosses
Example: Celestial bodies
Below is an additional example of one class definition to define another class and thus
linking together objects of different types. We recommend that you study this example
especially if you are unsure whether you understood the Order
and Treasure
examples
above or if you find yourself struggling with the game-themed assigment below. You won’t
miss any novel content by skipping this example, though.
The classes CelestialBody
and AreaInSpace
Just for practice, let’s create a couple of toy classes and create a two-dimensional model of Earth and its moon in space.
The plan is that instances of class CelestialBody
represent
individual planets or satellites. We’ll also have an AreaInSpace
class: an instance of that class represents a section of space
that contains the Earth and the Moon. An AreaInSpace
object will
be associated with exactly two CelestialBody
objects. The idea is
similar to how we associated each Order
object with one Customer
and each Treasure
object with one Monster
.
Here’s CelestialBody
, a very simple class:
class CelestialBody(val name: String, val radius: Double, var location: Pos) {
def diameter = this.radius * 2
override def toString = this.name
}
Pos
object: each CelestialBody
object stores a reference to a Pos
, which in turn
stores an x
and a y
.Here’s the AreaInSpace
class, which makes use of CelestialBody
:
class AreaInSpace(size: Int) {
val width = size * 2
val height = size
val earth = new CelestialBody("The Earth", 15.9, new Pos(10, this.height / 2))
val moon = new CelestialBody("The Moon", 4.3, new Pos(971, this.height / 2))
override def toString = s"${this.width}-by-${this.height} area in space"
}
AreaInSpace
object is created, it takes in a constructor
parameter that indicates its size (in coordinate units). For this
toy example, let’s say that any AreaOfSpace
we create is always
twice as wide as it is high. We’ll store the width and height in
the object’s instance variables.AreaInSpace
is associated with two CelestialBody
objects. In this simple
program, we’ll give those two objects specific names, radiuses,
and locations, which we spell out in the code.Once the classes have been so defined, we can use them in the REPL:
val space = new AreaInSpace(500)space: AreaInSpace = 1000-by-500 section of space space.earthres6: CelestialBody = The Earth space.moonres7: CelestialBody = The Moon
AreaInSpace
. As we do so...CelestialBody
objects, which the
AreaInSpace
object’s instance variables earth
and moon
refer to.We can then access those objects’ methods and variables:
space.earth.diameterres8: Double = 31.8 space.earth.locationres9: o1.Pos = (10.0,250.0) space.moon.locationres10: o1.Pos = (971.0,250.0) space.moon.radiusres11: Double = 4.3 space.moon.location.xDiff(space.earth.location)res12: Double = -961.0
AreaInSpace
has two celestial bodies that
we access through the corresponding variables.CelestialBody
objects has a Pos
object that we access through the object’s location
variable.In the next Chapter 2.7, there another optional example that continues from this one: we’ll build a graphical view of space using these classes as the domain model.
A Game Project
Let’s start working on a new module. You’ll see it incrementally across several chapters, in parallel with other modules such as GoodStuff and Odds. By the time we finish with this module, you’ll have programmed a playable game.
FlappyBug
Let’s create a game where the player controls a ladybug and tries to avoid obstacles. The ladybug makes a quick upward movement whenever the player commands it to flap its wings. Apart from that, though, the bug constantly sinks downwards, so the player has to keep flapping to keep it in the air. The bug moves only vertically; obstacles fly in horizontally from the right.
In this chapter, we’ll create a model of the program’s domain. That is, we’ll model the concepts of the game world (such as obstacle) and the operations associated with them (such as flying). We won’t build a user interface for the game just yet.
We’ll start with a simple version of the game that contains one bug and only one obstacle. In later chapters, you’ll both expand on this initial domain model and create a graphical user interface for the program.
Let’s now program three classes:
Bug
: defines the concept of a bug and the attributes and methods associated with it.Obstacle
: defines the concept of an obstacle.Game
: An instance of this class corresponds to a single game session and keeps track of the game’s overall state. Via aGame
object, we can access the parts of the game world (the bug and one obstacle). The game object determines how and when to activate the methods of those other objects as the game progresses.
An implementation for Obstacle
is provided as an example below. After studying it, you
get to write classes Bug
and Game
yourself.
Class Obstacle
When we create an obstacle, we set its size (radius) and give it an initial position within the two-dimensional coordinate system that covers the game world. Like this, for instance:
val bigObstacle = new Obstacle(150, new Pos(800, 200))bigObstacle: o1.Obstacle = center at (800.0,200.0), radius 150
In this simple version of the game, an obstacle isn’t capable of much anything, but it does know how to fly:
bigObstacle.approach()bigObstacleres13: o1.Obstacle = center at (790.0,200.0), radius 150 bigObstacle.approach()bigObstacle.approach()bigObstacleres14: o1.Obstacle = center at (770.0,200.0), radius 150
Obstacle
object has a mutable state that changes when we
call approach
on the object. Each invocation of the method
reduces the obstacle’s x coordinate by ten.Here’s an implementation for the class in pseudocode:
class Obstacle(a fixed value to store the radius; a gatherer to store the position) { def approach() = { Adjust the gatherer that keeps track of my current position: determine the new value from the old one by adding -10 to the x coordinate. } override def toString = combine the obstacle’s data into a description like the one in the example }
The same in Scala:
class Obstacle(val radius: Int, var pos: Pos) {
def approach() = {
this.pos = this.pos.addX(-10)
}
override def toString = "center at " + this.pos + ", radius " + this.radius
}
Before we continue to Bug
and Game
, there are two noteworthy things to discuss
about this class:
approach
’s definition
Why is that? On a related note, perhaps you already wondered why we
used a similar pair of brackets earlier as we called approach
in
the REPL.A constant is better than magic
We can define a constant to displace the magic number:
val ObstacleSpeed = 10
Where to write this definition? The approach that we’ll adopt here is to reserve a separate location for the various constants that affect the rules of the FlappyBug game.
You can find a partial implementation of the game in the FlappyBug module. The file
constants.scala
contains the above definition of ObstacleSpeed
. The obstacle class
is defined in another file, Obstacle.scala
; its method approach
uses the constant as
shown below.
import constants._
class Obstacle(val radius: Int, var pos: Pos) {
def approach() = {
this.pos = this.pos.addX(-ObstacleSpeed)
}
override def toString = "center at " + this.pos + ", radius " + this.radius
}
constants
. We import
them in this file.Interlude: Parameterless, Effectful Methods — And Brackets
The empty brackets are there because the method takes no parameters: it has an empty
parameter list. On the other hand, it’s true that we have created other parameterless
methods without empty brackets; these include toString
and description
, above, and
many others.
There is a convention among Scala programmers to provide a visual hint as to whether
a parameterless method is effectful or effect-free. When a parameterless method is
effectful (as approach
is), we mark this with an empty pair of round brackets in the
method’s definition. However, if a parameterless method is effect-free (like toString
or description
), we omit the brackets. This convention complements the punctuation
rules for functions that we established in Chapter 1.7.
We similarly either include or omit the empty brackets when we call a parameterless
method. The brackets in the method call bigObstacle.approach()
emphasize the fact
that this is a method call that impacts on obstacle object’s state.
The uniform access principle
If you want to know more about the reasoning behind the omission of brackets from some parameterless methods, you can read up on the uniform access principle (yhtenäisen osoituksen periaate). There is a Wikipedia article, for instance.
Conversely, the expression testCustomer.description
alone does not betray whether it
calls a method named description
or accesses a variable of that name. Scala’s authors
have specifically wished that calling an effect-free, parameterless method looks
identical to fetching the value an instance variable (which fetching also doesn’t
have an effect on state). There are reasons why this is a good idea; the easiest to
appreciate at this stage is convenience: it’s not necessary for the class’s user to
recall or care whether, say, description
is an instance variable or a method.
Adopt these conventions
You will need to observe the above conventions on the use of brackets. In particular, when a programming assignment specifies that you should write a method that has empty brackets as its parameter list, make sure you include the brackets in the method definition and also use the brackets when calling that method.
Assignment: FlappyBug (Part 1 of 17: Class Bug
)
How a Bug
should work
A bug object is created like this:
val myBug = new Bug(new Pos(300, 200))myBug: o1.Bug = center at (300.0,200.0), radius 15
As you can see in the text produced by toString
above, bugs are similar to obstacles in
that they have a location and a radius. We can examine these two attributes separately, too:
myBug.posres15: o1.Pos = (300.0,200.0) myBug.radiusres16: Int = 15
Pos
we
specified with the constructor parameter.A bug has a method for “flapping its wings”. For now, we’ll model the flight of the bug simply by reducing the bug’s y coordinate by whichever amount was given as a parameter.
myBug.flap(9.5)myBug.posres17: o1.Pos = (300.0,190.5) myBug.flap(20.5)myBug.posres18: o1.Pos = (300.0,170.0)
A bug is also capable of falling downwards. The fall
method increases the bug’s
y coordinate by two:
myBug.fall()myBug.posres19: o1.Pos = (300.0,172.0) myBug.fall()myBug.posres20: o1.Pos = (300.0,174.0)
Task description
Implement class Bug
in Bug.scala
of the FlappyBug module. It must work as described
above.
Instructions and hints
- Please use the specified names:
pos
,flap
, etc. This advice also applies to the other programming assignments that ask you to implement a class according to specification.- The bug’s position changes; let’s model this
with a
var
. The radius doesn’t change; use aval
.fall
andflap
are methods, sodef
is appropriate.
- The bug’s position changes; let’s model this
with a
- The class you need to write is in many ways similar to the obstacle class that we already created.
- Notice that
fall
is effectful and takes no parameters. Apply what you just learned about empty brackets in Scala. - You can use the constants defined in
constants.scala
in favor of magic numbers. - As you test your program, make sure you load the FlappyBug module in the REPL.
A+ presents the exercise submission form here.
Assignment: FlappyBug (Part 2 of 17: Class Game
)
An object of type Game
represents an overall state within our FlappyBug game. In this
version of the game, such a state comprises a single bug and a single obstacle. A Game
object also has methods for modifying the state: for example, when time passes, it directs
the bug to fall and the obstacle to advance. To that end, the Game
object stores
references that point to the other objects that it commands.
How a Game
should work
We don’t need constructor parameters to instantiate class Game
:
val testGame = new GametestGame: o1.Game = o1.Game@10eb1721
A newly created Game
object represents the game’s initial state, which is always this:
the game contains a ladybug at (100,40) and an obstacle with a radius of 70 at (1000,100).
The game object has instance variables (val
s) named bug
and obstacle
. They refer
to the bug and obstacle objects that are part of that gaming session:
testGame.bugres21: o1.Bug = center at (100.0,40.0), radius 15 testGame.obstacleres22: o1.Obstacle = center at (1000.0,100.0), radius 70
Calling the parameterless method timePasses
advances the game’s state by
making the bug fall and the obstacle approach from the right:
testGame.timePasses()testGame.bugres23: o1.Bug = center at (100.0,42.0), radius 15 testGame.obstacleres24: o1.Obstacle = center at (990.0,100.0), radius 70
Game
instance.A Game
object also has the method activateBug
, which we intend to call whenever
the human player issues a command to the game. When activateBug
is invoked on a
Game
object, it instructs the bug to use its wings with a “strength” of 15:
testGame.activateBug()testGame.bugres25: o1.Bug = center at (100.0,27.0), radius 15
Task description
Implement class Game
in Game.scala
.
Instructions and hints
- The class doesn’t take any constructor parameters, so you can
open the curly brackets of the class body right after
class Game
. This has already been done for you in the skeleton code provided inGame.scala
.
Something to think about
What would happen if you were to absent-mindedly
define two methods instead of the instance
variables bug
and obstacle
? (Cf. the Treasure
example above.)
- Define two instance variables (named
bug
andobstacle
) and two methods (namedactivateBug
andtimePasses
).- Remember that even though classes have
upper-case names (like
Bug
), those variables should start with a lower-case letter as inbug
.
- Remember that even though classes have
upper-case names (like
- Make the instance variables refer to
Bug
andObstacle
objects. Make sure you create instances of those classes.- To clarify: do not copy the code of class
Bug
or classObstacle
into classGame
. Instead, use those classes from withinGame
: usenew
to create one instance of each of the two classes. - You can use
new
as you define the instance variables:val variable = new ClassName(parameters)
- If you have difficulty understanding this
part, you may wish to refer back to the
optional
AreaInSpace
example above.
- To clarify: do not copy the code of class
- When implementing
activateBug
andtimePasses
, make sure you don’t re-implement the functionality that’s already available in classesObstacle
andBug
. (In particular, don’t do any arithmetic on coordinates.) Instead: call the methods of the bug and the obstacle! - Again, use empty brackets when you define or call effectful methods that take no parameters.
- You can use the constants defined in
constants.scala
. You may also wish to define additional constants there and use them. - If you feel that you don’t quite understand how we can use this class as a part of an actual graphical game, don’t worry. Our game still lacks a user interface, but we’ll address that soon.
A+ presents the exercise submission form here.
The package
Keyword
Many of the Scala files that you’ve edited begin with the word package
, as on this
line:
package o1
The meaning is fairly self-evident: the package
keyword is used at the top of
each Scala file to mark which package the contents belong to.
Any program components so defined as belonging to the same package can refer to
each other by their simple names; no import
necessary. For example, you just
defined Game
without having to import the other two classes, since Bug
and
Obstacle
belong to the same package as Game
.
These package
definitions are necessary but unnoteworthy. They’ve been largely
omitted from the code fragments in this ebook’s text. (Obviously, they are included
in the IntelliJ modules.)
So far, we’ve lumped pretty much all our code in the generic o1
package. We’ll
make better use of packages later.
Summary of Key Points
- You can consider variables from multiple angles: is it a
val
or avar
? What is its data type? Is it a local variable or an instance variable? What is the variable’s role in the program? - The majority of variables can be usefully described using one of a
dozen or so role labels.
- A fixed value can be used (among other things) for storing the immutable attribute of an object; a gatherer for accumulating a result by combining multiple inputs; a temporary for storing an intermediate result for a while; and a most-recent holder for keeping track of an attribute whose value may be replaced by another.
- Fixed-valued variables whose value is known before running the
program are commonly known as constants.
- Constants make code easier to understand and modify.
- In Scala, it’s customary to capitalize the names of constants.
- You can define instance variables whose type is defined by a custom
class that you wrote yourself.
- This establishes links between classes.
- If a class defines such an instance variable, each object of that type (e.g., each order) stores a reference to another object (e.g., a customer).
- Scala has certain rules and conventions concerning the use of round brackets in parameterless methods. Careful!
- Links to the glossary: variable, role, fixed value, temporary, gatherer, most-recent holder; constant, magic number; reference; model.
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, and Nikolas Drosdek with input from Juha Sorva, Otto Seppälä, Arto Hellas, and others.
For O1’s current teaching staff, please see Chapter 1.1.