This course has already ended.

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.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. Empty parameter lists in Scala.

What Will I Do? Read, first. Then, we’ll get started on a game project.

Rough Estimate of Workload:? Two 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.

Related Modules: IntroOOP, FlappyBug (new).

../_images/person06.png

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 vals and vars (Chapter 1.4). This is depicted below.

../_images/variables_valvar-en.png

(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:

../_images/variables_type-en.png

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.

../_images/variables_context-en.png

Roles of Variables

../_images/variables_role-en.png

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

../_images/fixed_value.png

A fixed value is set in stone.

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

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!"

In Scala, it’s customary to capitalize the names of constants (as the O1 style guide will also tell you).

A constant doesn’t need to be a number. It can store any type of immutable information.

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

../_images/temporary.png

OK, so this illustration works only in the Finnish edition of the ebook. In Finnish, if you put information “behind your ear”, it means you store it until you need it. Oh, well. Now you can (temporarily?) recall a Finnish idiom.

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 vals. 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. Depending on the program, the new data could be user input, a method parameter, or something else.

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.

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 = Employee("Issur Danielovitch", 1916, 12345)testEmployee: o1.classes.Employee = o1.classes.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.

../_images/most-recent_holder-en.png

A most-recent holder discards its earlier values.

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.

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 might be able to assist the reader by annotating roles as comments in code.

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?

More roles

Soon, in Chapter 3.1, you’ll encounter another role, the stepper. A bit later, we’ll discuss containers (Chapter 4.2), most-wanted holders (Chapter 4.2), and flags (Chapter 5.6).

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 = Customer("T. Tester", 12345, "test@test.fi", "Testitie 1, 00100 Testaamo, Finland")testCustomer: o1.classes.Customer = o1.classes.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 = Order(10001, testCustomer)exampleOrder: o1.classes.Order = o1.classes.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

The ordering customer’s description is a substring 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.classes.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 + ">"

Each customer object has a few fixed values as instance variables. It uses them to keep track of its attributes.

Now to the other class. Let’s first sketch it out as pseudocode:

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

An order object must keep track of the total effect of each addition to the price. We can do this by using an instance variable as a gatherer.

We’re working with a model that consists of objects of different classes. We need each order object to store a reference to a customer object.

Through the stored reference, we can consult the appropriate customer object as we form a description of the 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"
end Order

We use class 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 Orders 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

end Monster

We’re not going to build a full 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 = 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

Each treasure has a value represented by a Double. Moreover, each treasure has a challenge level, which is also a number.

A higher challenge level means that the treasure is guarded by a stronger troll.

We can ask a Treasure object to provide its value and its guardian.

A 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 = Monster("troll", this.challenge)

  def appeal = this.value / this.guardian.healthNow

  override def toString = "treasure (worth " + this.value + ") guarded by " + this.guardian

end Treasure

We use the guardian’s current health as we determine the treasure’s appeal.

The given class Treasure does work as shown in the REPL example above. However, it does not work as we intended in all respects.

Below is another sequence of commands that uses the Treasure class. It fails to work as intended. Work out why.

val myTreasure = Treasure(500.0, 100)
println(myTreasure.appeal)   // prints 5.0 as expected
myTreasure.guardian.sufferDamage(50)
myTreasure.guardian.sufferDamage(40)
println(myTreasure.appeal)   // does not print 50.0 as expected

We’d like the last line to print 50.0, reflecting the fact that the guardian has only ten of its original 100 health points left, which raises the treasure’s appeal to 500.0/10 or 50.0. Which of the following explanations is correct?

Does this slightly edited sequence of commands work?

val myTreasure = Treasure(500.0, 100)
println(myTreasure.appeal)
val monster = myTreasure.guardian
monster.sufferDamage(50)
monster.sufferDamage(40)
println(myTreasure.appeal)

The only difference between this command sequence and the previous one is that here we have an additional variable monster, which refers to the example treasure’s guardian and which we use as we invoke sufferDamage.

Which of the following correctly describes the code?

So, the treasure class isn’t in full working order. The problem is related to how the code says def guardian, thus defining a method that returns the treasure’s guardian. What if we had val guardian instead — that is, a guardian variable rather than a method?

Example: Bosses of bosses

We saw that it’s possible to “link” two separate classes by defining an instance variable in one class (such as Order) and using the other class (such as Customer) as the type of that variable. Could we also link a class to itself?

class Employee(val boss: Employee):

  // other stuff goes here

end Employee

In the above code, the variable has the class itself as its type. Does that make any sense? Think about it and take your best shot.

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

end CelestialBody

Celestial bodies have names, radiuses, and locations. The location is a 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 = CelestialBody("The Earth", 15.9, Pos(10,  this.height / 2))
  val moon  = CelestialBody("The Moon",   4.3, Pos(971, this.height / 2))

  override def toString = s"${this.width}-by-${this.height} area in space"

end AreaInSpace

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

Apart from the width and height, any instance of 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 = AreaInSpace(500)space: AreaInSpace = 1000-by-500 section of space
space.earthres6: CelestialBody = The Earth
space.moonres7: CelestialBody = The Moon

We create an AreaInSpace. As we do so...

... we also get two 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

The AreaInSpace has two celestial bodies that we access through the corresponding variables.

Each of the 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.

../_images/flappybug_sketch-en.png

The plan.

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 a Game 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 = Obstacle(150, Pos(800, 200))bigObstacle: o1.flappy.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.flappy.Obstacle = center at (790.0,200.0), radius 150
bigObstacle.approach()bigObstacle.approach()bigObstacleres14: o1.flappy.Obstacle = center at (770.0,200.0), radius 150

An 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

end Obstacle

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

end Obstacle

Before we continue to Bug and Game, there are two noteworthy things to discuss about this class:

One of the methods uses the magic number 10 that controls the speed of obstacles.

There is an empty pair of round brackets in 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.

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

end Obstacle

We can now use a named constant instead of a magic number.

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.

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.

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 = Bug(Pos(300, 200))myBug: o1.flappy.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

A newly created bug is initially located at the Pos that we specified with the constructor parameter.

At least in this version of our program, any bug always has an unchanging radius of 15.

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 a val. fall and flap are methods, so def is appropriate.

  • The class you need to write is in many ways similar to the obstacle class that we already created.

  • 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 = Game()testGame: o1.flappy.Game = o1.flappy.Game@10eb1721

To instantiate a class that doesn’t take any constructor parameters, we simply write an empty pair of brackets after the class name.

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 (vals) named bug and obstacle. They refer to the bug and obstacle objects that are part of that gaming session:

testGame.bugres21: o1.flappy.Bug = center at (100.0,40.0), radius 15
testGame.obstacleres22: o1.flappy.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.flappy.Bug = center at (100.0,42.0), radius 15
testGame.obstacleres24: o1.flappy.Obstacle = center at (990.0,100.0), radius 70

We observe a change in the coordinates of the bug and the obstacle that are associated with this 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.flappy.Bug = center at (100.0,27.0), radius 15

The bug’s y coordinate is 15 units less than it was.

Task description

Implement class Game in Game.scala.

Instructions and hints

  • The class doesn’t take any constructor parameters, so you can write the colon right after class Game. This has already been done for you in the skeleton code provided in Game.scala.

  • Define two instance variables (named bug and obstacle) and two methods (named activateBug and timePasses).

    • Remember that even though classes have upper-case names (like Bug), those variables should start with a lower-case letter as in bug.

  • Make the instance variables refer to Bug and Obstacle objects. Make sure you create instances of those classes.

    • To clarify: do not copy the code of class Bug or class Obstacle into class Game. Instead, use those classes from within Game: create one instance of each of the two classes.

    • You may create an object where you define an instance variable: val variable = ClassName(parameters)

    • If you have difficulty understanding this part, you may wish to refer back to the optional AreaInSpace example above.

  • When implementing activateBug and timePasses, make sure you don’t re-implement the functionality that’s already available in classes Obstacle and Bug. (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.

Summary of Key Points

  • You can consider variables from multiple angles: is it a val or a var? 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 and so contributed to this ebook’s design. Thank you!

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

The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.

The automatic assessment of the assignments has been developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Anna Valldeoriola Cardó, and Aleksi Vartiainen.

The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.

The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio did the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.

The other diagrams and interactive presentations in the ebook are by Juha Sorva.

The O1Library software has been developed by Aleksi Lukkarinen and Juha Sorva. Several of its key components are built upon Aleksi’s SMCL library.

The pedagogy of using O1Library for simple graphical programming (such as Pic) is inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and Krishnamurthi and Picturing Programs by Stephen Bloch.

The course platform A+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services. Markku Riekkinen is the current lead developer; dozens of Aalto students and others have also contributed.

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

For O1’s current teaching staff, please see Chapter 1.1.

Additional credits for this page

The FlappyBug game is inspired by the work of Dong Nguyen.

a drop of ink
Posting submission...