Luet oppimateriaalin englanninkielistä versiota. Mainitsit kuitenkin taustakyselyssä osaavasi suomea. Siksi suosittelemme, että käytät suomenkielistä versiota, joka on testatumpi ja hieman laajempi ja muutenkin mukava.
Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit.
Kieli vaihtuu A+:n sivujen yläreunan painikkeesta. Tai tästä: Vaihda suomeksi.
Chapter 5.1: Logic, Blood, and Shopping
Intro: Let’s Make FlappyBug Harder
In Chapter 3.3, you wrote isLost
, a method that defines a Game
of FlappyBug to be
over if the obstacle touches the bug.
def isLost = this.obstacle.touches(this.bug)
As things stand, it’s too easy for the bug to advance simply by lurking at the top or
bottom. Let’s edit touches
so that the player loses if an obstacle hits or the bug
is at the top or bottom of the playing area.
Here’s one way to do it:
def isLost =
if this.obstacle.touches(this.bug) then
true
else if this.bug.pos.y <= 0 then
true
else
this.bug.pos.y >= GroundY
The game is lost if the bug hits an obstacle.
Or if it didn’t hit an obstacle but is too high.
Or if it isn’t too high either but is too low.
That works. But it’s a complicated way of expressing something rather simple. We’d much rather write “if that condition or that other condition is met.” Perhaps you’ve already yearned after such an expression in the earlier chapters.
In this chapter, you’ll learn to combine Boolean
values with logical operators such
as “and” and “or”. We’ll get back to FlappyBug after a general introduction to these
operators.
Logical Operators
The Scala language supplies us with several logical operators (logiikkaoperaattori).
Operator |
Name |
Example |
Meaning |
---|---|---|---|
|
and |
|
“Are both Booleans |
|
or |
|
“Is at least one of the Booleans |
|
exclusive or (xor) |
|
“Is exactly one of the Booleans |
|
not (negation) |
|
“Is the Boolean |
In O1, you’ll mostly find use for &&
, ||
, and !
. The last of those you already know
from earlier chapters.
The logical operators take in Boolean
values and also produce Boolean
s:
val number = 50number >= 0 && number <= 5res0: Boolean = false if number >= 0 && number <= 5 then "a valid grade" else "an invalid grade"res1: String = "an invalid grade" number == 100 || number == 50res2: Boolean = true !(number >= 0)res3: Boolean = false
The “and” operator &&
produces true
if and only if both
subexpressions are true
. Here, the first subexpression is
true
but the second isn’t, so the whole expression evaluates
to false
.
The “or” operator ||
produces true
if either of the two
subexpressions is true
(or both are). Here, the first
subexpression isn’t true
but the second is, so the whole
expression evaluates to true
.
Terminological note: the subexpressions that an operator works
on are often called operands (operandi). Of the logical
operators introduced here, the negation operator !
takes only
a single operand while the others take two.
Non-strict evaluation of logical operators
The operators &&
and ||
are evaluated non-strictly (väljästi). What this means
for these operators is essentially that the left-hand operand is evaluated first and
if it alone determines the value of the logical operation, the right-hand operand
isn’t evaluated at all. (We will take a broader look at non-strict evaluation in
Chapter 7.2.)
This non-strictness has practical significance, as illustrated below.
First, let’s define a couple of variables. Here, we use literals, but you can easily imagine the numbers being read from the user’s keyboard, for instance.
val dividend = 50000dividend: Int = 50000 var divisor = 100divisor: Int = 100
Let’s divide one number with the other and see what we get. As we do so, let’s play
things safe and ensure that divisor
doesn’t equal zero. We can use either &&
or
||
to do that:
divisor != 0 && dividend / divisor < 10res4: Boolean = false divisor == 0 || dividend / divisor >= 10res5: Boolean = true
Assuming the divisor is not zero, we can swap the operands and still get the same result:
dividend / divisor >= 10 || divisor == 0res6: Boolean = true
But what if the divisor is zero?
divisor = 0divisor: Int = 0 dividend / divisor >= 10 || divisor == 0java.lang.ArithmeticException: / by zero ...
The computer performs the division as it evaluates the first operand. We get a runtime
error. We never even reached the expression divisor == 0
.
On the other hand, if we check the divisor in the first operand, things work out fine:
divisor == 0 || dividend / divisor >= 10res7: Boolean = true
Since divisor equalled zero, the expression divisor == 0
evaluated to true
. It
becomes unnecessary to evaluate the rest of the logical expression given that true ||
anythingAtAll
is always true
. Because of the non-strictness of these operators, the
computer doesn’t even evaluate the second operand. Execution order matters!
Combining logical operators
You can use rounds brackets to group logical expressions just like arithmetic and other expressions.
if (number > 0 && number % 2 == 0) || number < 20 then
println("Either both positive and even, or less than 20.")
if number > 0 && (number % 2 == 0 || number < 20) then
println("Positive; moreover, either even or less than 20.")
Practice on logical operators
Study the following piece of code. Carefully consider what happens as it runs. You might wish to make notes on a piece of paper.
val first = 20
val second = 10
var third = 50
if first > second && first + second < third then
third = 30
println("One")
val result = first > second && first + second < third
if first < second || result then
println("Two")
if first > second || second == third then
println("Three")
third = 10
if !(result && third >= 50) then
println("Four")
else
println("Five")
Further improvements to the vending machine
Logical operators are a convenient replacement for multiple if
s.
Recall the sellBottle
method from Chapter 3.5. Here is the original Int
-returning
version:
def sellBottle() =
if this.isSoldOut then
-1
else if !this.enoughMoneyInserted then
-1
else
this.earnedCash = this.earnedCash + this.bottlePrice
this.bottleCount = this.bottleCount - 1
val changeGiven = this.insertedCash - this.bottlePrice
this.insertedCash = 0
changeGiven
Here are two attempts to reimplement the method with logical operators:
def sellBottle() =
if this.isSoldOut || !this.enoughMoneyInserted then
-1
else
this.earnedCash = this.earnedCash + this.bottlePrice
// Etc. As above.
def sellBottle() =
if this.enoughMoneyInserted && !this.isSoldOut then
this.earnedCash = this.earnedCash + this.bottlePrice
// Etc. As above.
else
-1
Assignment: FlappyBug (Part 15 of 17: Deadly Edges)
Make FlappyBug harder as suggested at the top of the chapter. Do it as follows.
Add an effect-free, parameterless method
isInBounds
in classBug
. It should return aBoolean
that indicates whether the bug is within the legal playing area. The legal playing area is between the zero coordinate and the ground level (excluding those border values). Use a logical operator to implement the method.Modify
isLost
in classGame
so that it also considers a game to be lost if the bug is out of bounds. Use logical operators and theisInBounds
method you just wrote.
A+ presents the exercise submission form here.
Assignment: Blood Types
Task description
The module Blood contains several numbered packages. For now, we’ll concentrate on
o1.blood1
. Within that package, there’s a class named BloodType
. Implement that
class so that it matches the module’s documentation.
Instructions and hints
This example shows how the class should work:
import o1.blood1.*val myBlood = BloodType("AB", true)myBlood: o1.blood.BloodType = AB+ val yourBlood = BloodType("A", true)yourBlood: o1.blood.BloodType = A+ val theirBlood = BloodType("O", false)theirBlood: o1.blood.BloodType = O- myBlood.canDonateTo(yourBlood)res8: Boolean = false yourBlood.canDonateTo(myBlood)res9: Boolean = true theirBlood.canDonateTo(yourBlood)res10: Boolean = true
You can also run the given program (
test
) to try out all the possible combinations of blood types.Once again, you don’t have to attend to special cases or erroneous input unless specifically instructed to. For example, you can count on any user of class
BloodType
to pass in only meaningful strings as constructor parameters.Try to implement the methods as simply as you can and without duplicating the same piece of code.
Use logical operators. Can you make do without
if
andmatch
altogether (apart fromtoString
)?Call the other methods of the same class. Further tip: you can pass
this
as a parameter to a method.
A+ presents the exercise submission form here.
Assignment: FixedPriceSale
The Scaladocs in module AuctionHouse1 describe several classes for representing items that have been put up for sale in an imaginary online auction house. In this assignment, we’ll focus on a single way to sell an item: simply giving it a fixed price. Later assignments will feature auctions and changing prices.
Task description
Fetch the AuctionHouse1 module and view its Scaladocs. Implement class FixedPriceSale
of package o1.auctionhouse
so that its behavior matches the documentation. Write your
solution in FixedPriceSale.scala
.
Instructions and hints
You may wish to use an instance variable of type
Option
, with an initial value ofNone
. If you do, remember that you need to annotate the variable with a data type (as in Chapter 4.3). Like this:nameOfVariable: Option[Type]
.Additional hints about instance variables
You should make two instance variables of the three constructor parameters. You’ll find these two public variables listed in the docs.
You’ll need a couple of more variables: one to track the number of remaining days and another to store the buyer’s name.
Since there isn’t necessarily any buyer,
Option[String]
is a suitable type for the latter variable.The two variables just mentioned aren’t listed in the docs; they are part of the class’s internal implementation. Make them private and name them as you please.
The Scaladocs list the class’s members in alphabetical order, but that’s probably not the most convenient order for you to implement them (nor do the methods need to be in that order in your code). You are free to choose the order in which you implement the methods, but here’s one suggestion:
toString
,daysLeft
,buyer
,isExpired
,isOpen
,advanceOneDay
,buy
.Hints about the methods
Study the docs with care. Notice, for instance, that “open” and “expired” aren’t exact opposites even though an item cannot be both at the same time.
Most of the methods are effect-free. They only need to return the value of an instance variable or check whether the instance variables meet a certain condition. A single-line expression will do as the implementation for each of these methods.
Items have two effectful methods:
advanceOneDay
changes the number of days remaining andbuy
changes who’s bought the item. You should have each of those things available in instance variables. Remember, in both methods, to check whether the sale was open; you can callisOpen
to do that.You may use the program
testFixedPrice
for testing. Use it.That program initially generates error messages, which is simply because you haven’t yet implemented the methods that the program calls. The errors will go away once you do the assignment. (Remember: the cause of an error is not always where the message points to.)
You can initially “comment out” part of the test program if you want to test a partial version of your class.
You can also run
TestApp
, a program ino1.auctionhouse.gui
that lets you create and manipulateFixedPriceSale
objects in a graphical window that looks like this:
A+ presents the exercise submission form here.
Optional Practice: Moment
and Interval
The rest of the chapter consists of practice tasks that are voluntary but highly recommended. Take them as an opportunity to build up fluency. If the weekly deadline is ominously close, you might wish to do the other chapters first before returning here. The assignments below will probably take several hours to complete.
Introduction to o1.time
This problem will let you apply the logical operators more extensively and freely than the preceding ones. It also provides practice on a number of other concepts.
Moments and intervals
Imagine we have an application (or several) that needs to represent time intervals:
an interval begins at a particular moment of time and continues until another moment.
We intend to model these concepts as the classes Moment
and Interval
. Each
Interval
object is associated with two Moment
objects that represent the interval’s
first and last moment.
The Miscellaneous module contains a package o1.time
and, therein, a partial
implementation for class Moment
. Interval
is missing entirely.
The given code for Moment
is explained below.
import scala.math.abs
class Moment(private val time: Int):
override def toString = this.time.toString
def distance(another: Moment) = abs(another.time - this.time)
def isLaterThan(another: Moment) = this.time > another.time
def later(another: Moment) = if this.isLaterThan(another) then this else another
def earlier(another: Moment) = if this.isLaterThan(another) then another else this
end Moment
As we create a Moment
instance, we specify the moment in time
that it stands for. Our implementation doesn’t commit to any
particular unit of time (days, years, nanoseconds, etc.); any
integer will do.
A Moment
delegates its whole toString
method to the integer
it stores. Notice that you can call a method on an Int
! An
Int
’s toString
method returns the characters that correspond
to the number; e.g., for the integer 123 we get the string "123"
.
(We will discuss the methods on Int
s further in the next
chapter, 5.2.)
There’s a distance
method for determining how far apart two
moments are.
isLaterThan
, later
, and earlier
compare moments in
different ways.
General hint: make use of abstractions
One of this problem’s main themes is to learn to make use of the other methods in the classes that you write. Sure, you’ve already done that in ealier assignments, too, but this one particularly highlights the matter.
If you write the required methods completely independently of each other, you’ll find yourself duplicating code. Don’t do that. Consider what other methods you’ve created that you could call. Try to keep your code DRY. Non-redundant code is easier to modify and less vulnerable to bugs.
In fancier words: Every method is an abstraction (Chapter 1.6). You should build new
abstractions atop existing ones. For example, the method isLaterThan
is as an
abstraction of its concrete implementation, which takes the values of two instance
variables and uses an operator to compare the two: this.time > another.time
. The other
methods that need to compare two Moment
s can simply use this method and don’t need to
deal with the variables directly.
A task in four phases
Implement Interval
and Moment
so that they meet the specification. Below is a
recommended workflow that you can either follow or ignore.
Phase 1 of 4: Getting started with Interval
Read the introductions of both classes in the Scaladocs. Also read the docs for
Interval
’s methodslength
andtoString
, which you’ll implement first.Create a new file for class
Interval
.Define the constructor parameters. Since each parameter needs to be stored in an instance variable, define those, too, in the class header. Should these instance variables be private?
Implement
length
. Use a method from classMoment
.Now and later: resist the temptation to add public members in class
Moment
beyond those request in the docs. In particular, don’t make the instance variabletime
public.
Implement
toString
. It’s a bit more complicated. Here are some hints:Use the methods you have at your disposal.
You can use an
if
–else
structure to cover the three different cases.Remember that a string can be “multiplied”. For instance,
"ho" * 3
yields"hohoho"
.
See the starter code in
timeTest
. Add commands there as you see fit: createInterval
objects and call their methods.
When are two Moment
s equal?
If you use ==
or !=
to compare moments, what you get is an identity comparison
(“Do these two references point to the same Moment
object?”; see Chapter 3.3.)
What you don’t get is a comparison of the Moment
objects’ actual contents. These
operators won’t tell you whether two distinct Moment
objects represent the same
moment in time (i.e., whether their time
variables have identical values).
In this assignment, identity comparison and those two operators are completely unnecessary. You don’t stand to gain anything from using them. Other methods will serve you better!
Phase 2 of 4: isLaterThan
and overloading
The interval assignment was quite a tranformative experience.
I was already well on my way, writing an if
statement in
each method and not reusing methods, when a couple of formidable
and eagle-eyed course assistants, who just happened to be in the
room, pointed this out to me.
I didn’t dare defy the wise assistants so I racked my brain,
sweating, to figure out how it could even be possible to manage
without an if
. Once I came up with a solution for isLaterThan(Interval)
(helped by a hefty dose of hints from the assistant), I felt like
I had just invented the wheel. It was ingenious!
Add
isLaterThan
in classInterval
. Note that the method name has been overloaded (Chapter 4.1): there are two methods with the same name. One takes in aMoment
and the other anInterval
. Implement the former first. Make use of an existing method.Implement the second
isLaterThan
method inInterval
. Make use of the method you implemented just before. As you do so, remember a detail from Chapter 4.1: if an overloaded method calls its namesake, Scala requires an explicit type annotation for the calling method’s return type. That means you’ll need to annotate the type on at least one of theisLaterThan
methods.Add more test code to
timeTest
and run it. Confirm that bothisLaterThan
methods work as intended.Moment
should also have anisLaterThan
method that takes in anInterval
parameter. Add that method to the class.
Phase 3 of 4: logical operations
Continue implementing methods on Interval
and Moment
:
Of the two
contains
methods ofInterval
, implement the one that takes in aMoment
. Use logical operators in combination with methods that you have previously implemented.Implement the other
contains
method. Again, use logical operators and the other methods. Also recall what was just said about overloaded methods.Implement
isIn
in classMoment
. It essentially does “the same thing in reverse” as another method you already wrote, which gives you a very simple implementation for it.Implement
overlaps
in classInterval
. Once again, you can find uses for logical operators and existing methods. Make sure you cover all the possible cases.Add more method calls in
timeTest
and test your new methods.
Phase 4 of 4: combining instances to produce new ones
The methods union
and intersection
are still missing from class
Interval
. Implement them and you’re done.
A+ presents the exercise submission form here.
How private is private
?
Student question: How can that Moment
class work? time
is private!
Let’s take a moment to consider why the given code from the previous assignment doesn’t produce an error message.
class Moment(private val time: Int):
def isLaterThan(another: Moment) = this.time > another.time
time
is private
. It’s not available for external use.
But what counts as external, exactly?
A Moment
object can access its own time
.
A Moment
object can also access the time
of another
Moment
object: we simply prefix the variable name with
an expression that refers to another object of the same
class. Here, that expression is the variable name another
.
Within a class’s code, the private
modified indicates that a member
of the class is meant for internal use within that class. The private
member isn’t accessible by only a single object but also by the class’s
other instances; it is private to them collectively.
Optional Practice: Auctions
Assignment: DutchAuction
Go back to AuctionHouse1. Implement class DutchAuction
as specified.
Instructions and hints:
First and foremost: make sure you understand what “Dutch auctions” are! The details are in the Scaladocs. Unless you have a clear idea of what the class should do, implementing it will be tough going.
As you read the docs, you’ll notice that some parts of
DutchAuction
are identical toFixedPriceSale
. You can copy parts of your earlier solution into yourDutchAuction
class.You can again use
TestApp
fromo1.auctionhouse.gui
to experiment with your class. You can of course also write your own text-based program for testing.If you have an
Int
and need aDouble
, you can either
A+ presents the exercise submission form here.
Are you a bit upset that you had to write the same methods again in class
DutchAuction
or at least copy them from one file to the other? Outstanding!
That means that you’re motivated to reach Chapter 7.3 where you’ll learn
about avoiding such code duplication.
Assignment: EnglishAuction
Stay with AuctionHouse1 and implement EnglishAuction
.
Instructions and hints:
Again, first make sure you understand exactly how “English auctions” are expected to work.
A simple
Bid
has been provided, which you should find useful. There is also a bit of starter code forEnglishAuction
.Even though it’s possible to solve the assignment with a buffer that stores all bids placed on an item, you don’t actually need to do that. As it turns out, you don’t need to solve every last bid to fullfill the requirements...
Instead, you can use two variables to store just the two highest bids, as shown in the starter code. Notice these two variables’ initial values: when there are no actual bids yet, the variables refer to “empty”
Bid
s equal to the starting price.TestApp
will again be of service.
A+ presents the exercise submission form here.
Summary of Key Points
Logical operators operate on Boolean values. You can use them to express meanings such as “X and Y” and “X or Y”.
Links to the glossary: logical operator; to overload; DRY.
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.
Recap:
touches
returnstrue
orfalse
. Consequently,isLost
also returns one of the twoBoolean
s.