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 4.4: Exercises in Not Existing

../_images/person08.png

Assignment: Improve Class VendingMachine

You may recall that after we created class VendingMachine in Chapter 3.5, we raised some questions about the quality of its sellBottle method.

Locate VendingMachine in module Miscellaneous and modify it. Edit sellBottle so that it no longer returns minus one to signify a failed purchase. Instead, the method’s return type should be Option[Int]; the method should return None if no bottle was sold and the amount of change wrapped in a Some object if a bottle was sold.

A+ presents the exercise submission form here.

Assignment: Fix Class Member

The same module contains class o1.people.Member, a separate example. Examine its program code and documentation. You’ll find that the code doesn’t match the docs: the methods isAlive and toString are missing. Write them.

The match command is one way to solve this assignment, but if you use a couple of the methods on Option, you should be able to come up with a still simpler solution. The methods were introduced in the previous chapter (4.3).

A+ presents the exercise submission form here.

Assignment: Implement Class Passenger

Task description

A class Passenger is listed in the documentation for o1.people. No matching code has been given, however. Implement the class.

Instructions and hints

  • The class uses another class, TravelCard. That class has been provided for you. Use it as is; don’t change it.

  • The documentation details Passenger’s constructor parameters and their corresponding public instance variables. You won’t need to define any private instance variables for this class.

  • As stated in the Scaladocs, passengers should have an instance variable of type Option[TravelCard]; that is, each passenger has zero or one travel cards. Option works as a wrapper for our custom class TravelCard.

  • You need to create a file for class Passenger within o1.people. Here’s how:

    1. Right-click the package in IntelliJ’s Project view and select New → Scala Class/File. A small dialog pops up.

    2. IntelliJ will set up the file for you after you enter some additional information in the dialog. At the Name prompt, enter Passenger and press Enter.

    3. A new file shows up in the editor, with a bit of starter code.

  • Refer to Chapter 4.3 for tools that you can use to implement the methods.

  • If you find yourself struggling with how to access isValid, you may want to check out the additional hints below.

A first hint about isValid

You have a value of type Option[TravelCard]. That Option object does not have an isValid variable; the TravelCard object that it may contain does. The expression this.card.isValid thus does not work.

Handle the Some and None cases separately. Access isValid on the Some object’s contents.

A further hint about isValid

From Chapter 4.3: When you handle the Some case using match, you can extract the Some object’s contents into a variable and pick a name for the variable.

Use that variable to access isValid, as in variableName.isValid.

A+ presents the exercise submission form here.

Assignment: Improve Class Order

There’s nothing new about the problem below, but it provides further practice on Options and match. We recommend it especially if you had difficulty with the above assignments. You can also come back to practice on this problem if you run into trouble later on.

Additional practice

Return to class Order in module IntroOOP.

There was a tiny optional assignment in Chapter 2.6 where the description method in class Order was replaced by a toString method.

If you didn’t do that then, do it now: rename description to toString and write override in front. Also do this for class Customer.

Now edit the class as follows:

  • Add a constructor parameter address with the type Option[String]. Also introduce a corresponding val instance variable. This variable will to store a postal address that is (possibly) assosiated with the order.

  • Add a parameterless, effect-free method deliveryAddress. It should return a String that indicates where the order should be delivered. This will be either the address associated with the order, if there is one, or the orderer’s personal address, if the order isn’t associated with an address.

  • Edit the toString method so that there’s an additional bit at the end: a comma and a space ", " followed by either "deliver to customer's address" or "deliver to X", where X is the address associated with the order.

A+ presents the exercise submission form here.

Assignment: Convenient Keyboard Control

The next optional assignment is meaningful only if you have done the Trotter assignment from Chapter 3.6. Of course, you could do that assignment now if you didn’t already.

Direction.fromArrowKey

Package o1 contains not just the class Direction but also a singleton object of the same name. The singleton object has a convenient method fromArrowKey, whose behavior is illustrated below.

val exampleKey = Key.UpexampleKey: o1.Key.Value = Up
Direction.fromArrowKey(exampleKey)res0: Option[Direction] = Some(Direction.Up)
Direction.fromArrowKey(Key.X)res1: Option[Direction] = None

As you see, the method returns the direction that corresponds to the given key on the keyboard. It wraps the return value in an Option, because only some keys correspond to a direction.

Edit TrotterApp’s onKeyDown method in module MoreApps. The method should do the same job as before but you can write a simpler and less redundant implementation with fromArrowKey.

A+ presents the exercise submission form here.

Assignment: Star Maps (Part 1 of 4: Basic Star Info)

Introduction: stars and their coordinates

Let’s write a program that displays star maps: views of the night sky. A star map contains a number of stars; stars may also be linked together to form constellations.

In this first part of the assignment, we won’t be drawing anything yet. We’ll begin by creating some tools for representing individual stars.

Fetch the Stars module. For now, we’ll concentrate on classes StarCoords and Star: you use the former, which is already implemented, to implement the latter. Study the two classes’ documentation; don’t mind the other classes now.

As indicated in the Scaladocs, our program needs to work on two different sorts of two-dimensional coordinates:

../_images/star_coords.png

In StarCoords, each coordinate is between minus one and plus one. For instance, if the x of a star is +1.0 and its y is 0.0, it’s located at the middle of a star map’s right-hand edge.

  1. A star’s location on a two-dimensional star map is represented as a StarCoords object.

    • For this, we use a coordinate system like the one you know from math class, with values of y increasing towards the top.

    • We assume that all values of x and y have been normalized so that they fall within the interval [-1.0...+1.0]. (See the illustration.) The two coordinates represent a star’s location in the visible sky independently of the size of any picture that may depict the sky.

  2. On the other hand, you can use the method toImagePos of a StarCoords instance to produce a Pos that represents the star’s location within a particular Pic. These coordinates increase, like the other Pos coordinates that we’ve used, right- and downward from the top left-hand corner. They indicate which pixel the star’s center should appear at within a larger Pic of the entire sky that has a known width and height.

Task description

  1. Read the above and the Scaladocs. Make sure you understand the two coordinate systems we need. Make sure you understand what toImagePos in StarCoords accomplishes.

  2. Then implement the missing methods of class Star so that their behavior meets the Scaladoc specification.

Instructions and hints

  • Here is an example of how your Star class should work:

    val unnamedStar = Star(28, StarCoords(0.994772, 0.023164), 0.1, None)unnamedStar: o1.stars.Star = #28 (x=0.99, y=0.02)
    unnamedStar.posIn(rectangle(100, 100, Black))res2: o1.world.Pos = (99.7386,48.8418)
    unnamedStar.posIn(rectangle(200, 200, Black))res3: o1.world.Pos = (199.4772,97.6836)
    val namedStar = Star(48915, StarCoords(-0.187481, 0.939228), -1.44, Some("SIRIUS"))namedStar: o1.stars.Star = #48915 SIRIUS (x=-0.19, y=0.94)
    
  • You don’t actually have to implement the required math yourself if you make good use of StarCoords.

  • You also don’t need to round a star’s coordinates even though toString returns them in rounded form. The toString method in StarCoords already does that for you, so use it.

Formatting Doubles within Strings

You may want to take a look at toString in class StarCoords, which rounds the coordinates to two decimal points. That method uses an f notation that you can read more about in an article on different forms of string interpolation.

A+ presents the exercise submission form here.

Assignment: Star Maps (Part 2 of 4: Seeing Stars)

Task description

The Stars module contains the package o1.stars.gui. The file skypics.scala in that package defines functions that we’ll use to generate pictures of star maps.

In this short assignment, we’ll focus on placeStar, a function that takes a picture and a star as parameters. The method returns a modified version of the picture. That new picture is identical to the original except that it has a picture of the star drawn on top. (We’ll be drawing the stars in the sky as circles.)

For instance, let’s say we want a picture that contains pictures of these two stars:

val unnamedStar = Star(28, StarCoords(0.994772, 0.023164), 0.1, None)unnamedStar: o1.stars.Star = #28 (x=0.99, y=0.02)
val namedStar = Star(48915, StarCoords(-0.187481, 0.939228), -1.44, Some("SIRIUS"))namedStar: o1.stars.Star = #48915 SIRIUS (x=-0.19, y=0.94)

The following code should do the trick.

val darkBackground = rectangle(500, 500, Black)darkBackground: Pic = rectangle-shape
val skyWithOneStar = placeStar(darkBackground, unnamedStar)skyWithOneStar: Pic = combined pic
val skyWithTwoStars = placeStar(skyWithOneStar, namedStar)skyWithTwoStars: Pic = combined pic
skyWithTwoStars.show()

placeStar lacks an implementation. Read its specification in the Scaladocs and implement it in skypics.scala where indicated.

Instructions and hints

This part-assignment should be easy if you use the tools introduced in Part 1 above.

Try running the above example. If your code works, there should be one star near the top of the picture (that’s namedStar) and a smaller one in the middle of the right-hand edge (unnamedStar).

Reflection

After writing the method, you may reflect on what we have and have not accomplished so far: we can now display individual stars in a star map, but it would take a lot of manual work to display a map with more than a sprinkling of stars.

It would be much more convenient if we could load star data into our program from a repository of some sort. And that is indeed what we will do soon enough. (Feel free to take a look at the folders test and northern and especially the stars.csv files in those folders. We’ll return to this program in Chapter 5.2.)

A+ presents the exercise submission form here.

Assignment: Football3

Our football-scores application (of Chapter 3.5) was already rewritten once (in Chapter 4.2), and now we’re going to mess with it again.

Task description

Fetch Football3. Study its documentation. The Match class is now somewhat different than before, and there is a new class, Season.

Implement the two classes so that they meet the new specification.

This assignment is submitted in two parts: first Match, then Season.

../_images/module_football3.png

A diagram of the main relationships between classes in module Football3.

Instructions and hints: the reworked Match class

  • You may copy and paste your Match implementation from Football2 into Football3 and work from there. Just make sure that your new code starts with package o1.football3 (not football2).

  • The old Match code should work except that you’ll need to make changes to the winner-related methods:

    • In addition to winnerName, there is a new method named simply winner, which returns an Option.

    • winningScorerName no longer exists. It’s replaced by winningScorer, which returns an Option.

    • (Once you have winner working, you may use it to simplify winnerName.)

  • Remember: Match with a capital M is a programmer-chosen name for a class that represents football matches. match is a Scala command (that can’t be used as a name; it’s a reserved word). Mix up these two, and you may get some interesting error messages.

  • You may again use FootballApp for testing.

A+ presents the exercise submission form here.

Instructions and hints: the Season class

  • Once you’ve implemented Season, the FootballApp’s main window will display season statistics and a list of matches.

  • When implementing Season, you may find it useful to review Chapters 4.2 and 4.3 and the GoodStuff application. There are similarities between Season and GoodStuff’s Category.

  • There are a few optional hints available below.

A generic hint for the biggestWin method

GoodStuff’s Category class kept track of the experience with the highest rating. The Season class, in comparison, keeps track of the match with the biggest win margin. In this respect, these two programs are very similar.

For working out the biggest win, you can use an instance variable of type Option as a most-wanted holder, much like we did in class Category. You can update that variable in addResult much as the Category class does in its addExperience method.

Hints for comparing Matches to one another

Did you notice that goalDifference can return a negative number? The match with the biggest win margin is the one whose goalDifference’s absolute value is the greatest.

To compute absolute values, recall that you have the function scala.math.abs available.

You can either compare the matches in addResult itself or you can define a helper method to perform the comparison and call that method.

A hint for fixing IndexOutOfBounds errors that crash your program

An index-out-of-bounds error means that you’ve tried to access a collection index that is either too large or (probably in this case) too small.

Did you remember to consider the possibility that a season may have no matches at all yet?

If this didn’t help enough, you may want to check the next hint as well.

A hint for matchNumber and latestMatch

These two methods should return a match within an Option wrapper. To achieve that, you don’t need an if expression or anything complicated. See Chapter 4.3 for a method that lets you access an element of a collection safely as an Option.

By the way, did you notice that latestMatch is a special case of matchNumber? You can implement the former by calling the latter.

A+ presents the exercise submission form here.

Something to think about

The documentation goes out of its way to say that matches added to a Season are assumed to have finished and no more goals will be added to those matches. What happens if you go against this assumption? What would it take to reimplement the program so that we prevent that from happening?

Further Reading: The Flexible match Command

The boxes below tell you more about the match command, which we’ve used only for manipulating Option objects. This additional information isn’t required for O1 but should interest at least those readers who have prior programming experience and wish to explore Scala constructs in more depth. Beginners can perfectly well skip this section and learn about these topics at a later date.

match is a pattern-matching tool

Here is the general form of the match command:

expression E match
  case pattern A => code to run if E’s value matches pattern A
  case pattern B => code to run if E’s value matches pattern B (but not A)
  case pattern C => code to run if E’s value matches pattern C (but not A or B)
  And so on. (Usually, you’ll seek to cover all the possible cases.)

match can examine the value of any expression. In technical terms, this examination is a form of pattern matching (hahmonsovitus): the expression’s value is compared to...

... so-called patterns (hahmo) that define different cases. For now, we have defined only cases with None and Some patterns, but many more kinds of patterns are possible. Some of them are introduced below.

Primitive matching on literals

Suppose we have an Int variable called number. Let’s use match to examine the value of the expression number * number * number.

val cubeText = number * number * number match
  case 0         => "number is zero and so is its cube"
  case 1000      => "ten to the third is a thousand"
  case otherCube => "number " + number + ", whose cube is " + otherCube

match checks the patterns in order until it finds one that matches the expression’s value. Here, we have a total of three patterns.

Even a simple literal can be used as a pattern. Here, we’ve used a couple of Int literals. The first case is a match if the cube of number equals zero; the second matches if the cube equals one thousand.

You can also enter a new variable name as a pattern; here, we’ve picked the name otherCube. Such a pattern will match any value; in this example, the third case will always be selected if the cube wasn’t zero or one thousand.

Whenever such a pattern matches, you get a new local variable that stores the actual value that matched the pattern. You can use the variable name to access the value.

Below is a similar example where we match on Boolean literals rather than Ints. These two expressions do the same job:

if number < 0 then "negative" else "non-negative"
number < 0 match
  case true  => "negative"
  case false => "non-negative"

Let’s return to our cubeText example and reorder the last two cases.

val cubeText = number * number * number match
  case 0         => "the number is zero and so is its cube"
  case otherCube => "number " + number + ", whose cube is " + otherCube
  case 1000      => "ten to the third is a thousand"

Which of the following claims are correct (assuming number stores a value of type Int)? Select all that apply.

How about this version?

val magicNumber = 27
val cubeText = number * number * number match
  case 0           => "the number is zero and so is its cube"
  case magicNumber => "the cube equals magicNumber, which is " + magicNumber
  case 1000        => "ten to the third is a thousand"
  case otherCube   => "number " + number + ", whose cube is " + otherCube

Use the REPL to explore the following claims. Select all correct ones.

We can accomplish all that by chaining ifs and elses (Chapter 3.4), too. Matching on literals and variables has not yet demonstrated the power of match. But see below.

Question from student: Is match roughly the same as Java’s switch?

Java and some other programming languages have a switch command that selects among multiple alternative values that an expression might have. Scala’s match is similar in some ways. However, switch can select only a case that corresponds to a specific value (as in our cubeText example), whereas match provides a more flexible pattern-matching toolkit. Perhaps the most significant differences are that match can:

  • make a selection based on an object’s type; and

  • “take apart” the object and automatically extract parts of it into variables defined in the pattern.

Examples of both appear later in this chapter.

Guarding a case with a condition

You can associate a pattern with an additional condition (a pattern guard) that needs to be met for a match to happen.

val cubeText = number * number * number match
  case 0                  => "the number is zero and so is its cube"
  case 1000               => "ten to the third is a thousand"
  case other if other > 0 => "positive cube " + other
  case other              => "negative cube " + other

The condition narrows down the case: we select this branch only if the value is greater than zero (and doesn’t equal 1000, which we already covered in another case). Note that we use the familiar if keyword, but this isn’t a standalone if command.

The last case will be selected only if the value is non-zero and not 1000 or any other positive number.

Underscores in patterns

An underscore pattern means “anything” or “don’t care what”. Here are a couple of examples.

number * number * number match
  case 0    => "the number is zero and so is its cube"
  case 1000 => "ten to the third is a thousand"
  case _    => "something other than zero or thousand"

The underscore pattern matches any value and is selected if neither of the two preceding cases is. We could have written the name of a variable here (as we did in earlier examples), but if we have no use for the variable’s value, an underscore will do.

matching on data types

In the preceding examples, the patterns corresponded to different values of the same type. In this example, the patterns match values of different types:

def experiment(someSortOfValue: Matchable) =
  someSortOfValue match
    case text: String              => "it is the string " + text
    case number: Int if number > 0 => "it is the positive integer " + number
    case number: Int               => "it is the non-positive integer " + number
    case vector: Vector[?]         => "it is a vector with " + vector.size + " elements"
    case _                         => "it is some other sort of value"

Our example function’s parameter has the type Matchable, which means that we can pass more or less any value as a parameter. (Anything that can be processed with match goes; this covers nearly all Scala classes.)

The patterns have been annotated with data types. Each of these patterns matches only values of a particular type.

The variables defined in the patterns have the corresponding type. For example, the variable vector has the type Vector, so we can use the variable to call the matching vector’s size method.

Using match to take apart an object

One of match’s most appealing features is that you can use it to destructure the object that matches a pattern, extracting parts of it into variables. A simple example is the “unwrapping” of a value stored within an Option:

vectorOfNumbers.lift(4) match
  case Some(wrapped) => "the number " + wrapped
  case None          => "no number"

The pattern defines the structure of the matched object: a Some will have some value inside it. That value is automatically extracted and stored in the variable wrapped.

This feature of match can be combined with the others listed above. Below, we take apart an Option and, at the same time, try to match its possible contents to one of several cases:

vectorOfNumbers.lift(4) match
  case Some(100)                         => "exactly one hundred"
  case Some(wrapped) if wrapped % 2 == 0 => "some other even number " + wrapped
  case Some(oddNumber)                   => "the odd number " + oddNumber
  case None                              => "no number at all"

You can destructure an object in this fashion only if the class defines how to do that for objects of that type. Many of Scala’s library classes come with such a definition, Some included. Similarly, O1’s own library defines that a Pos object can be destructured into two coordinates:

myPos match
  case Pos(x, y) if x > 0 => "a pair of coordinates where x is positive and y equals " + y
  case Pos(_, y)          => "some other pair of coordinates where y equals " + y

“If it’s a Pos, two numbers can be extracted from it. Store them in local variables x and y. If x is positive, this case matches.”

“Exctract two numbers from the Pos. Discard the first and store the other in a variable y.” Any Pos will match this pattern and this branch will be chosen every time if the first one isn’t.

So how do you define how objects of a particular type can be taken apart? The easiest way is to turn a class into a so-called case class (tapausluokka). Here’s a simple example:

case class Album(val name: String, val artist: String, val year: Int):
  // ...

Apart from the extra case, this is like a regular class definition.

The constructor parameters of a case class also perform a second role: they define how to destructure objects of this type. The name, artist, and year of any album object can be extracted while matching.

You can then use the case class like so:

myAlbum match
  case Album(_, _, year) if year < 2000 => "ancient"
  case Album(name, creator, released)   => creator + ": " + name + " (" + released + ")"

Our pattern definitions use the case class’s name, and their structure matches the class’s constructor parameters.

Search online for Scala pattern matching and Scala case class for more information. As noted, you aren’t required to use these language features in O1.

Further Reading: Option.get

This optional section introduces get, a method on Options that may seem deceptively handy but that you should avoid. In fact, you must not use this method in any of O1’s programming exercises (the little one right below is an exception). You won’t need get for anything. The method is introduced here only as a warning, because students sometimes run into it in other online tutorials and then end up using it unnecessarily and producing poor code.

The dangerous get method

One way to open up an Option wrapper is to call its parameterless get method. Try it in the REPL.

Assume the following definitions:

val first  = Some(10)
val second = None

What is the value of first.get?

What about the value of second.get?

We could have used get instead of match to implement addExperience in class Category:

def addExperience(newExperience: Experience) =
  this.experiences += newExperience
  val newFave =
    if this.fave.isEmpty then
      newExperience
    else
      newExperience.chooseBetter(this.fave.get)
  this.fave = Some(newFave)

isEmpty checks whether an old favorite exists.

We get the old favorite from its wrapper. Since we do this only in the else branch, we know we’re not dealing with None.

However, if we use get, we’re faced with some of the same problems that we had with null: if we call get on None, our program crashes. It’s up to the programmer to ensure that get is only called on a Some. This is easy to forget.

As one student put it:

Can’t open the wrapper if there’s no candy.

At least you shouldn’t just rush to open a wrapper, as get does. It’s better to be prepared for possible disappointment and avoid the tears.

It’s good to know that get exists; you may see it used in programs written by others. Not all programs you’ll see are of high quality. Avoid using the method yourself. There is always a better solution such as match, getOrElse, or one of the methods from Chapter 8.3.

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.

a drop of ink
Posting submission...