The latest instance of the course can be found at: O1: 2024
- CS-A1110
- Supplementary Pages
- Scala Reference
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.
Scala Reference
After studying the actual chapters of O1’s ebook and then turning to a programming problem, you may find yourself thinking “How was I supposed to write that thing again?” When that happens, you may want to check this page. The sections below summarize selected features of the Scala language and its standard libraries. There are short, isolated examples of each feature.
The reference doesn’t cover the entire Scala language; it focuses on topics that are covered in O1. In addition to standard Scala constructs, a few of the main tools in O1’s own auxiliary library are included.
This appendix of the ebook is just a list of tools. It won’t teach you any principles, or concepts, nor will it tell you what you may want to use all these constructs for; those are the sorts of things that you can learn in the ebook proper. The ordering of the sections on this page is not identical to the order in which the constructs appear in the ebook chapters.
Can’t find what you’re looking for?
You may find it through these links:
In the long run, you may want to learn to read the Scala Language Specification and the Scala API documentation. Parts of those documents are hard for a beginner programmer to make sense of, however.
If you wanted to find something on this page that isn’t here, you can let us know through the feedback form at the bottom of the page or directly via email to juha.sorva@aalto.fi.
Sections on This Page
- The Very Basics
- Packages and Libraries
- Function Basics
- Singleton Objects
- Classes (and more about objects)
- Image Manipulation with O1Library
- Truth Values
- Dealing with Missing Values
- Selection:
if
andmatch
- Scopes and Access Modifiers
- Pairs and Other Tuples
- More about Strings
- Collection Basics
- Common Methods on Collections
- Checking size:
size
,isEmpty
, andnonEmpty
- Element lookup:
contains
andindexOf
- Parts of a collection:
head
,tail
,take
,drop
,slice
, etc. - Adding elements and combining collections
- Copying elements in a new collection:
to
,toVector
,toSet
, etc. - Miscellaneous methods:
mkString
,indices
,zip
,reverse
,flatten
, etc.
- Checking size:
- More on Functions
- Processing Collections with Higher-Order Methods
- Repeating an operation:
foreach
- Turning elements into something else:
map
,flatMap
- The properties of collection elements:
exists
,forall
,filter
,takeWhile
, etc. - Relative order of elements:
maxBy
,minBy
,sortBy
- Generic processing of elements:
foldLeft
andreduceLeft
Option
as a collection- Creating elements with a function:
tabulate
- Repeating an operation:
- Lazy-Lists and Related Topics
- Repeating Commands in a Loop
Map
s- Supertypes and Subtypes
- Random Numbers
- Working with Files
- Graphical User Interfaces
- Reserved Words
- Feedback
- Credits
The Very Basics
Numbers
Basic arithmetic (Chapter 1.3):
100 + 1res0: Int = 101 1 + 100 * 2res1: Int = 201 (1 + 100) * 2res2: Int = 202
Dividing an Int
(integer) with another Int
chops off any decimals and effectively
rounds towards zero:
76 / 7res3: Int = 10
The modulo operator %
produces the remainder of a division (Chapter 1.7):
76 % 7res4: Int = 6
Double
s have decimals (up to a limit):
76.0 / 7.0res5: Double = 10.857142857142858
See Chapter 5.4 for the constraints that govern numerical data types’ range and precision. For the methods available on these types, see Chapter 5.2.
Characters and strings
A String
is a sequence of characters (Chapter 1.3). String
s have the operators
+
and *
:
"mam" + "moth"res6: String = mammoth "moth" * 3res7: String = mothmothmoth
The Char
type represents individual characters (Chapter 5.2). A Char
literal goes
in single quotation marks:
'a'res8: Char = a '!'res9: Char = !
For more on strings and characters, see Methods on string objects, Collection Basics, and Processing Collections with Higher-Order Methods further down on this page.
Variables
A variable definition (Chapter 1.4):
val myNumber = 100myNumber: Int = 100
You may mark a data type on the variable explicitly, as shown below, but due to Scala’s type inference, you often don’t need to:
val anotherVariable: Int = 200anotherVariable: Int = 200
You can use a variable’s name as an expression. Such an expression can be part of a longer expression:
myNumberres10: Int = 100 myNumber + anotherVariable + 1res11: Int = 301
You can use val
or var
to define a variable. The value of a val
never changes, but
the value of a var
may be assigned a new value, which replaces the old one:
var changeable = 100changeable: Int = 100 changeable = 150changeable: Int = 150 changeable = changeable + 1changeable: Int = 151
The command just above assigns the variable a new value that is obtained from the variable’s old value with a simple computation. There is a shorthand for such commands, which combines the assignment with the arithmetic operator (Chapter 4.1):
changeable += 10changeable: Int = 161 changeable -= 100changeable: Int = 61 changeable *= 2changeable: Int = 122
Comments
Program code may contain comments, which don’t affect the program’s behavior (Chapter 1.2).
// This is a single-line comment. It starts with a couple of slashes and runs until the end of the line.
val myVariable = 100 // You can follow a line of code with a comment.
/* A comment like this, which starts with a slash and
an asterisk, may be split over multiple lines of code.
The comment ends in the same characters in reverse order. */
An initial /**
marks a documentation comment (Chapter 3.2):
/** This description of the variable below will appear in the documentation. */
val myText = "I have been documented."
The Scaladoc tool extracts such comments from Scala code and uses them in the documents that it creates.
Packages and Libraries
Using packages
When you want to use one of the tools (functions, classes, etc.) from Scala’s standard
library (Chapter 3.2) or from some other package, you can prefix the tool’s name
with the name of the package. Here, we access the abs
function in package scala.math
to compute an absolute value:
scala.math.abs(-50)res12: Int = 50
Since the contents of the package scala
are always available in all Scala programs,
we’re allowed to leave out the first bit and just refer to the subpackage math
:
math.abs(-50)res13: Int = 50
The universal package scala
contains basic data types such as Int
and Double
,
collection types such as Vector
and List
, and the output function println
. You
can use these tools without specifying the package. For instance, even though you
could write scala.Int
, you don’t have to.
To avoid having to write a package name repeatedly, you can import
:
import
ing from a package
You can import
the tools you need from a package (Chapter 1.6):
import scala.math.absimport scala.math.abs abs(-50)res14: Int = 50 abs(100)res15: Int = 100
This gives us access to all the tools in the package:
import scala.math._import scala.math._
It’s common to write import
statements at the top of the code file, which makes the
imported tools available within the entire file. You can also import
within a specific
context; for example, starting a function body with an import
makes the imported tools
available in that function only.
Defining a package
You can sort your own code in packages by marking the package at the top of each file (Chapter 2.6). Here’s an example:
package mystuff.subpackage.subsubpackage
You then need to store these files in nested folders so that the folder names match the packages.
Scala moreover allows us to use individual objects as packages; see the section Package objects, below.
Commonly used functions from scala.math
A few frequently used functions from scala.math
:
import scala.math._import scala.math._ val absoluteValue = abs(-50)absoluteValue: Int = 50 val power = pow(10, 3)power: Double = 1000.0 val squareRoot = sqrt(25)squareRoot: Double = 5.0 val sine = sin(1)sine: Double = 0.8414709848078965 val greaterNumber = max(2, 10)greaterNumber: Int = 10 val lesserNumber = min(2, 10)lesserNumber: Int = 2
In the same package, you’ll find other trigonometric functions (cos
, atan
, etc.),
cbrt
(cubic root), hypot
(hypotenuse of two given legs), floor
(rounding down),
ceil
(rounding up), round
(rounding to nearest integer), log
and log10
(logarithms).
The complete list is in Scala’s documentation.
The other sections on this page introduce contents from the Scala API’s other packages as appropriate for each topic.
The text console: println
, readLine
You can use println
to generate a custom printout in the text console or the REPL:
println(100 + 1)101 println("llama")llama
Below are a few examples of reading keyboard input in the text console (Chapter 2.7).
These examples assume an earlier import scala.io.StdIn._
.
println("Please write something on the line below this prompt: ")
val textEnteredByUser = readLine()
If you don’t want a line break between the prompt and the input, you can use print
instead:
print("Please write something after this prompt, on the same line: ")
val textEnteredByUser = readLine()
This does the same thing:
val textEnteredByUser = readLine("Please write something after this prompt, on the same line: ")
readLine
returns a String
. You can also read an input and immediately interpret it as
a number:
val intInput = readInt()
val doubleInput = readDouble()
These two last commands cause a runtime error if the characters in the input don’t correspond to a valid number.
Function Basics
A simple function
An example function from Chapter 1.7:
def average(first: Double, second: Double) = (first + second) / 2
Calling a function
average(10.0, 12.5)res16: Double = 11.25
Multiple lines in a function
When the function body consists of several consecutive commands, you need to put them in curly brackets. Here’s an example from Chapter 1.7:
def incomeTax(income: Double, thresholdIncome: Double, baseRate: Double, additionalRate: Double) = {
val baseTax = min(thresholdIncome, income)
val additionalTax = max(income - thresholdIncome, 0)
baseTax * baseRate + additionalTax * additionalRate
}
If the function is an effectful one, it’s customary to write the curly brackets around the function body even when it’s not required; see the style guide.
Function parameters
The example functions above had a single parameter list (in round brackets after the function name). That parameter list may be empty (Chapter 2.6):
def printStandardMessage() = {
println("This is printed out every time we call printStandardMessage() .")
}
A function may not have a parameter list at all. (This is more common when the function is a method on an object; Chapter 2.2.)
def returnStandardText = "Calling returnStandardText always yields this string."
Or there may be multiple parameter lists (Chapter 6.1):
def myFunc(first: Int, second: String)(additionalParameter: Int) = first * additionalParameter + second
myFunc(10, "llama")(100)res17: String = 1000llama
Return values
In all of the examples above, we left the function’s return type implicit, which we can do because of type inference. But we may choose to explicitly mark the return type (Chapter 1.8), as shown here:
def average(first: Double, second: Double): Double = (first + second) / 2
def returnStandardText: String = "Calling returnStandardText always yields this string."
In certain contexts, an explicit return type annotation is mandatory. Primarily, this happens when a function calls a function of the same name; that is, the function calls either:
- another function with the same name but different parameters (when overloading a name; Chapter 4.1); or
- itself (in a recursive function; Chapter 12.1).
Explicitly return
ing a value
It’s possible (but not usual in Scala) to explicitly instruct a function
to return a value. The return
command (Chapter 8.3) interrupts the
function call and returns a value.
def incomeTax(income: Double, thresholdIncome: Double, baseRate: Double, additionalRate: Double): Double = {
val baseTax = min(thresholdIncome, income)
val additionalTax = max(income - thresholdIncome, 0)
return baseTax * baseRate + additionalTax * additionalRate
}
return
with the expression whose value should be returned.return
needs a return type annotation.Singleton Objects
Defining an object: methods, variables, and this
Here is a definition of an singleton object taken from an example in Chapter 2.2. (That chapter discusses the example in more detail.)
object employee {
var name = "Edelweiss Fume"
val yearOfBirth = 1965
var monthlySalary = 5000.0
var workingTime = 1.0
def ageInYear(year: Int) = year - this.yearOfBirth
def monthlyCost(multiplier: Double) = this.monthlySalary * this.workingTime * multiplier
def raiseSalary(multiplier: Double) = {
this.monthlySalary = this.monthlySalary * multiplier
}
def description =
this.name + " (b. " + this.yearOfBirth + "), salary " + this.workingTime + " * " + this.monthlySalary + " e/month"
}
object
keyword with a name that we’ve chosen for
our object.val
) and others mutable (var
).def
.this
keyword refers to the object itself: the object whose
method is being executed. For instance, the value of this.name
is the value of the object’s own name
variable. (It’s not
strictly necessary to always include the word this
in all such
expressions; see Chapter 2.2.)Using a singleton object: dot notation
You can access the variables of an object:
employee.monthlySalaryres18: Double = 5000.0 employee.workingTime = 0.6employee.workingTime: Double = 0.6
And call the object’s methods:
employee.raiseSalary(1.1)employee.ageInYear(2021)res19: Int = 56
Launching an application: app objects
An app object (Chapter 2.7) is a singleton object that serves as an application’s entry point:
object MyTestProgram extends App {
println("These lines of code are executed when the application is launched.")
println("This simple app does nothing more than print out these lines of text..")
println("In a richer app, we could call other objects’ methods here.")
}
extends App
makes this an app object. (To be more precise, it
mixes in the App
trait; see Traits, further down.)Package objects
A singleton object can serve as a package for assorted tools such as functions, objects,
and classes (Chapter 5.2). Such a package object can be defined as a regular singleton
object. Here, we define a package named mystuff.experiment
:
package mystuff
object experiment {
def doubled(number: Int) = number * 2
def tripled(number: Int) = number * 3
}
experiment
,
which we intend to use as a package object.The above code needs to be stored within a folder named mystuff
; we might name the file
experiment.scala
, for instance. We can now import
the tools in our object:
import mystuff.experiment._import mystuff.experiment._ doubled(10)res20: Int = 20 tripled(10)res21: Int = 30
An alternative way to define a package object
Another way to define a package object is to repeat the package
keyword as shown:
package mystuff
package object experiment {
def doubled(number: Int) = number * 2
def tripled(number: Int) = number * 3
}
A package object thus defined needs to be placed in a folder that matches
the name of the package; it might go in mystuff/experiment/package.scala
,
for example. People often name such files package.scala
, but a different
name will also work.
Classes (and more about objects)
Defining a class
Here is an example class from Chapter 2.4; it represents employees. Each instance of
this class is a distinct object of type Employee
and has its own attributes:
class Employee(nameParameter: String, yearParameter: Int, salaryParameter: Double) {
var name = nameParameter
val yearOfBirth = yearParameter
var monthlySalary = salaryParameter
var workingTime = 1.0
def ageInYear(year: Int) = year - this.yearOfBirth
// Etc. Other methods go here.
}
class
precedes the class name. The curly brackets
around the class body are mandatory.new
,
we need to pass in a name, a year, and a salary.Employee
object as 1.0, independently of any
parameters.this
refers to the
specific object that runs the method. For instance, ageOfYear
computes an employee’s age from the yearOfBirth
of whichever
Employee
object we invoke ageOfYear
on.There is a more compact notation for class definitions (Chapter 2.4):
class Employee(var name: String, val yearOfBirth: Int, var monthlySalary: Double) {
var workingTime = 1.0
def ageInYear(year: Int) = year - this.yearOfBirth
// Etc. Other methods go here.
}
Creating and using instances
We can use the above Employee
class as shown below (Chapter 2.3):
new Employee("Eugenia Enkeli", 1963, 5500)res22: o1.Employee = o1.Employee@1145e21
new
to instantiate the class, following it with the
name of the class and values for the constructor parameters.We can store such a reference to a newly created object in a variable. Then we can use the variable’s name to access the object:
val justHired = new Employee("Teija Tonkeli", 1985, 3000)justHired: o1.Employee = o1.Employee@704234 justHired.ageInYear(2021)res23: Int = 36 println(justHired.description)Teija Tonkeli (b. 1985), salary 1.0 * 3000.0 e/month
Tailoring an instance
It’s possible to define methods that are specific to an individual instance of a class.
Here’s an example class from Chapter 2.4:
class Person(val name: String) {
def say(sentence: String) = this.name + ": " + sentence
def reactToKryptonite = this.say("What an odd mineral.")
}
A regular Person
doesn’t know how to fly, but the following specific Person
instance
does. Moreover, one of its methods works differently than the same method on other
Person
objects.
val realisticSuperman = new Person("Clark") {
def fly = "WOOSH!"
override def reactToKryptonite = "GARRRRGH!"
}
Person
as usual except that we tailor this instance
within the curly brackets that follow.Person
instance has fly
as an additional method.Person
class, a fact that we need to mark
with the override
keyword.(This is actually an example of inheritance; see Chapter 7.3._)
Scala’s basic types as objects; operator notation
Basic types such as Int
, Double
, and String
are classes, too, and the operations
defined on them are methods (Chapter 5.2). For instance, when we use the +
method
to add two numbers, it’s possible to use dot notation:
1.+(1)res24: Int = 2
The more familiar expression 1 + 1
also works: when a method takes exactly one parameter,
we can opt to use operator notation and omit the dot and the brackets. This also works on
methods that we wrote ourselves:
justHired ageInYear 2021res25: Int = 36
Image Manipulation with O1Library
The IntelliJ module O1Library is a software library that has been designed for O1 and that we use frequently. It contains an assortment of tools for graphical programming, among other things.
The relevant contents of O1Library are introduced in various chapters of the ebook; you can also look at the module’s documentation. What appears below is a short summary of some of the features you’re most likely to need in O1:
Colors: o1.Color
The Color
class represent colors (Chapter 1.3). The o1
package provides many specific
instances of this class as constants:
import o1._import o1._ Redres26: Color = Red CornflowerBlueres27: Color = CornflowerBlue
These named color constants cover all the colors listed in W3C’s CSS Color Module standard and some others as well.
You can also define a color as a combination of its RGB components (Chapter 5.4). Each component is a number between 0 and 255, inclusive. Below, we create a fairly bright color that is especially high in red and blue:
val preciselyTheColorWeWant = Color(220, 150, 220)preciselyTheColorWeWant: Color = Color(220, 150, 220)
You can access the color’s individual components:
preciselyTheColorWeWant.redres28: Int = 220 CornflowerBlue.blueres29: Int = 237
In addition to their R, G, and B components, colors have an opacity
value (sometimes
called the alpha channel):
Red.opacityres30: Int = 255
val translucentRed = Color(255, 0, 0, 100)translucentRed: Color = Color(255, 0, 0, opacity: 100)
opacity
of only a hundred
is fairly translucent. An opacity of zero
would have made it completely transparent;
An opacity of 255 means the color is ompletely
opaque, which is the default.Locations: o1.Pos
The class o1.Pos
represents locations in a two-dimensional coordinate system
(Chapter 2.5).
val first = new Pos(15.5, 10)first: Pos = (15.5,10.0) val second = Pos(0, 20)second: Pos = (0.0,20.0)
Pos
object is essentially a pair of coordinates, each of
which is a Double
.new
to instantiate Pos
(because the class comes with a factory method;
see Chapter 5.3).You can examine each coordinate separately:
first.xres31: Double = 15.5 first.yres32: Double = 10.0
You can compute on Pos
objects:
val distanceAlongX = second.xDiff(first)distanceAlongX: Double = 15.5 val distanceAlongY = second.yDiff(first)distanceAlongY: Double = -10.0 val distanceAsCrowFlies = first.distance(second)distanceAsCrowFlies: Double = 18.445866745696716 val aBitToTheRight = first.addX(1.5)aBitToTheRight: Pos = (17.0,10.0) val adjustedBoth = aBitToTheRight.add(10, -5)adjustedBoth: Pos = (27.0,5.0)
None of the above methods changes the existing Pos
objects; neither does any other
method. The add
method, for example, doesn’t modify the existing Pos
but generates
a new one. Pos
objects are immutable.
For more methods, see, e.g., Chapter 3.1 and the documentation.
Pictures: o1.Pic
The class o1.Pic
represents images.
You can load an image from a file or a network address (Chapter 1.3):
val loadedFromFileInModule = Pic("face.png")loadedFromFileInModule: Pic = face.png
val loadedFromAbsoluteFilePath = Pic("d:/kurssi/GoodStuff/face.png")loadedFromAbsoluteFilePath: Pic = d:/kurssi/GoodStuff/face.png
val loadedFromTheNet = Pic("https://en.wikipedia.org/static/images/project-logos/enwiki.png")loadedFromTheNet: Pic = https://en.wikipedia.org/static/images/project-logos/enwiki.png
pics
folder of the O1Library
module, or somewhere else in the program’s classpath.Pic
s have a width and a height in pixels:
loadedFromTheNet.widthres33: Double = 135.0 loadedFromTheNet.heightres34: Double = 155.0
To display an image, you can use o1.show
or the method of the same name on Pic
objects:
show(loadedFromTheNet)loadedFromTheNet.show()
There are several functions available that generate images of geometric shapes. Here are a few examples:
val myCircle = circle(250, Blue)myCircle: Pic = circle-shape val myRectangle = rectangle(200, 300, Green)myRectangle: Pic = rectangle-shape val myIsoscelesTriangle = triangle(150, 200, Orange)myIsoscelesTriangle: Pic = triangle-shape val myStar = star(100, Black)myStar: Pic = star-shape val myEllipse = ellipse(200, 300, Pink)myEllipse: Pic = ellipse-shape
The Pic
methods that combine images by placing them relative to each other (Chapter 2.3)
see a lot of use in O1. Here are some examples:
val circleBesideRect = myCircle.leftOf(myRectangle)circleBesideRect: Pic = combined pic val circleBelowRect = myCircle.below(myRectangle)circleBelowRect: Pic = combined pic val circleInFrontOfRect = myCircle.onto(myRectangle)circleInFrontOfRect = combined pic
The methods don’t modify any existing image; they create new Pic
objects.
You can also place an image against a background image (Chapter 2.5):
val littlePic = rectangle(10, 20, Black)littlePic: Pic = rectangle-shape val littlePicAgainstBg = myRectangle.place(littlePic, Pos(30, 80))littlePicAgainstBg: Pic = combined pic val withAnAddedCircle = littlePicAgainstBg.place(myCircle, Pos(150, 150))withAnAddedCircle: Pic = combined pic
place
where in the background image it should
place the front image. Here, we do that by passing in a pair of
coordinates (in which x grows rightwards and y downwards). The
front image’s middle will appear at those coordinates in the
combined image.place
discards the part that doesn’t fit from the result.Here is a partial list of the methods available on Pic
objects:
- Placement on a single plane:
above
,below
,leftOf
,rightOf
(Chapter 2.3). - Placement in front of and behind:
onto
,against
,place
(Chapters 2.3 and 2.5). - Placement using anchors (e.g., “Put the top-left corner of this pic at the center of that pic’s top edge.”): see the end of Chapter 2.5.
- Rotation:
clockwise
,counterclockwise
(Chapter 2.3). - Mirroring:
flipHorizontal
,flipVertical
(Chapter 2.3). - Scaling:
scaleBy
(Chapter 2.3),scaleTo
. - Selecting a part:
crop
(Chapter 2.5). - Shifting along a coordinate axis:
shiftLeft
,shiftRight
(Chapter 3.1). - Examining individual pixels:
pixelColor
(Chapter 5.4). - Transforming by pixel:
transformColors
,combine
(Chapter 6.1). - Generating from pixels:
Pic.generate
(Chapter 6.1).
The complete list is in the Scaladocs.
Other classes in package o1
In addition to Color
, Pos
, and Pic
, the o1
package contains other
tools that are useful for creating graphical programs. In particular:
- The class view
View
provides a framework for writing GUIs. See the section Graphical User Interfaces further down on this page. - The class
Direction
represents (arbitrary) directions in a two-dimensional,Pos
-based coordinate system (see Chapters 3.6 and 4.4 and the docs). - The class
Grid
represents two-dimensional grids of elements (Chapter 7.4; Scaladocs). It’s works in combination with two additional classes: - The class
Anchor
represents “anchoring points” of images within other images and can make it easier to lay outPic
s relative to each other (Chapter 2.5; Scaladocs).
Truth Values
The Boolean
type
You can represent truth values with the Boolean
data type (Chapter 3.3). There are
exactly two values of this type, true
and false
, each of which has its own Scala
literal.
falseres35: Boolean = false val theValueOfThisVariableIsTrue = truetheValueOfThisVariableIsTrue: Boolean = true
Relational operators
Relational operators produce Boolean
s (Chapter 3.3):
10 <= 10res36: Boolean = true 20 < (10 + 10)res37: Boolean = false val age = 20age: Int = 20 val isAdult = age >= 18isAdult: Boolean = true age == 30res38: Boolean = false 20 != ageres39: Boolean = false
!=
checks if the values are not equal.Logical operators
Logical operators (from Chapter 5.1):
Operator | Name | Example | Meaning |
---|---|---|---|
&& |
and | someClaim && otherClaim |
“Are both Booleans true ?” |
|| |
or | someClaim || otherClaim |
“Is at least one of the Booleans true ?” |
^ |
exclusive˽or (xor) | someClaim ^ otherClaim |
“Is exactly one of the Booleans true ?” |
! |
not (negation) | !someClaim |
“Is the Boolean false ?” |
Examples:
val dividend = 50000dividend: Int = 50000 var divisor = 100divisor: Int = 100 !(divisor == 0)res40: Boolean = true divisor != 0 && dividend / divisor < 10res41: Boolean = false divisor == 0 || dividend / divisor >= 10res42: Boolean = true dividend / divisor >= 10 || divisor == 0res43: Boolean = true
The operators &&
and ||
are non-strict: if the subexpression on the left is enough
to determine the value of the entire logical expression, the subexpression on the right
isn’t evaluated at all:
divisor = 0divisor: Int = 0 dividend / divisor >= 10 || divisor == 0java.lang.ArithmeticException: / by zero ... divisor == 0 || dividend / divisor >= 10res44: Boolean = true divisor != 0 && dividend / divisor < 10res45: Boolean = false
Dealing with Missing Values
Option
, Some
, and None
The following example function has the return type Option[Int]
(Chapter 4.3). The
function either divides two numbers and returns the result wrapped in a Some
object,
or returns None
in case the operation is impossible:
def divide(dividend: Int, divisor: Int) =
if (divisor == 0) None else Some(dividend / divisor)
divide(100, 5)res46: Option[Int] = Some(20) divide(100, 0)res47: Option[Int] = None
Here, we use Option
as a wrapper for a String
:
var test: Option[String] = Nonetest: Option[String] = None test = Some("like it hot")test: Option[String] = Some(like it hot)
Option[String]
can refer either to
the singleton object None
— in which case there is no
string there — or a Some
object that contains a string.Option
wrapper.Option
we’d like
as the type of test
.Scala makes it possible to use the null
reference instead of Option
s; however, it’s
highly unadvisable that you do so (Chapter 4.3).
Methods on Option
objects
The methods isDefined
and isEmpty
check whether an Option
wrapper is full or empty:
val wrappedNumber = Some(100)wrappedNumber: Option[Int] = Some(100) wrappedNumber.isDefinedres48: Boolean = true wrappedNumber.isEmptyres49: Boolean = false None.isDefinedres50: Boolean = false None.isEmptyres51: Boolean = true
getOrElse
returns the value stored in an Option
wrapper. When we call it, we need to
pass in a parameter expression that determines what the method should return in case the
wrapper is empty:
wrappedNumber.getOrElse(12345)res52: Int = 100 None.getOrElse(12345)res53: Int = 12345
The similar method orElse
returns the Option
object itself, in case it’s a Some
,
and the value of the method parameter in case the Option
is None
. That is, the
difference to getOrElse
is that orElse
doesn’t unwrap the value:
wrappedNumber.orElse(Some(54321))res54: Option[Int] = Some(100) None.getOrElse(Some(54321))res55: Option[Int] = Some(54321)
More on Option
The following are also useful for working with Option
s:
- the selection command
match
, discussed soon below; and - various higher-order methods, which are discussed further
down on this page at
Option
as a collection.
Selection: if
and match
Selecting with if
The if
command (Chapter 3.4) evaluates a conditional expression that evaluates to
true
or false
, then selects one of two options on that basis:
val number = 100number: Int = 100
if (number > 0) number * 2 else 10res56: Int = 200
if (number < 0) number * 2 else 10res57: Int = 10
Boolean
expression as a condition.You can use an if
expression as you assign to variables or pass parameters to functions,
just like you can use other expressions:
val selected = if (number > 100) 10 else 20selected: Int = 20 println(if (number > 100) 10 else 20)20
When a branch of an if
contains multiple commands, you need to split the branch across
multiple lines and put it in curly brackets (and it’s customary to do that anyway in case
the if
is effectful; see the style guide):
if (number > 0) { println("The number is positive.") println("More specifically, it is: " + number) } else { println("The number is not positive.") }The number is positive. More specifically, it is: 100
When all you want to do is to cause an effect in case the condition is true
— and
nothing otherwise — you can omit the else
:
if (number != 0) {
println("The quotient is: " + 1000 / number)
}
println("The end.")The quotient is: 10
The end.
println
isn’t part of the if
; it follows the if
.
This is why the above program always finishes with "The end."
,
no matter whether number
holds zero or not. If number
had been
zero, that would have been the program’s only output.Combining if
s
One way to select among multiple alternatives is to put an if
in another if
’s
else
branch:
val number = 100number: Int = 100 if (number < 0) "negative" else if (number > 0) "positive" else "zero"res58: String = positive if (number < 0) { println("The number is negative.") } else if (number > 0) { println("The number is positive.") } else { println("The number is zero.") }The number is positive.
There are other ways to nest an if
inside another, too:
if (number > 0) {
println("Positive.")
if (number > 1000) {
println("More than a thousand.")
} else {
println("Positive but no more than a thousand.")
}
} Positive.
Positive but no more than a thousand.
if
has no else
branch at all. If
number
hadn’t been positive, nothing would have been printed
out.In the example above, the else
branch connected with the “closer” of the two if
s.
That else
branch was selected precisely because the outer condition was true
but the
inner one wasn’t. In the next example, which places the curly brackets differently, the
inner if
has no else
branch but the outer one does:
if (number > 0) { println("Positive.") if (number > 1000) { println("More than a thousand.") } } else { println("Zero or negative.") }Positive.
For a further discussion, see Chapter 3.4. The end of Chapter 3.5 lists some examples
of errors that you may make when you use an if
to determine the function’s return value.
Selecting with match
The match
command (Chapters 4.3 and 4.4) evaluates an expression and then checks
a list of possible matches for that value. It selects the first one that matches.
Here’s what the command looks like in general terms:
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
keyword
is compared to...Here’s an example as concrete code:
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 value of the expression. Here, we have a total of
three patterns.Int
literals. The first case is a match if the cube
of number
equals zero; the second matches if it equals one
thousand.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.One use for match
is to extract a value from an Option
wrapper:
// We need this function for the example of match below.
def divide(dividend: Int, divisor: Int) =
if (divisor == 0) None else Some(dividend / divisor)
divide(firstNumber, secondNumber) match {
case Some(result) => "The result is: " + result
case None => "No result."
}
Some
will have some value inside it. That value is
automatically extracted and stored in the variable result
.(However, for working with Option
s, higher-order methods are often even better than match
;
see Chapter 8.2 and Option
as a collection, below.)
Here’s one more example that demonstrates some more features of match
. The example is
from Chapter 4.4, which you can visit for more optional material on this versatile command.
def experiment(someSortOfValue: Any) =
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"
}
Any
, which means
that we can pass a value of any type as a parameter.if
keyword, but this isn’t a standalone
if
command.Scopes and Access Modifiers
Program components — variables, functions, classes, and singleton objects — each have
a scope that depends on where that component is defined (Chapter 5.6). The programmer may
further adjust scope by adding access modifiers such as private
(Chapter 3.2).
The scope of a class and its members
class MyClass(constructorParameter: Int) {
val publicInstanceVariable = constructorParameter * 2
private val privateInstanceVariable = constructorParameter * 3
def publicMethod(parameter: Int) = parameter * this.privateMethod(parameter)
private def privateMethod(parameter: Int) = parameter + 1 + this.privateInstanceVariable
}
myObject.publicInstanceVariable
.
Similarly, we can call a public method anywhere within the class or
outside of it. Instance variables and methods are public unless
otherwise specified.The scope of local variables
Mouse over the boxes below to highlight the corresponding scope within the program.
def myFunc(param: Int) = {
var local = param + 1
var anotherLocal = local * 2
if (local > anotherLocal) {
val localToIf = anotherLocal
anotherLocal = local
local = localToIf
}
anotherLocal - local
}
param
is defined throughout
the function’s body. It is accessible anywhere within that
scope.local
, runs until the end of the
function body.anotherLocal
.localToIf
whose scope
is limited by the surrounding if
.You’ll find a few more complex examples in Chapter 5.6.
Local functions
As discussed in Chapter 6.4, you can also define functions as local to other functions. Here’s a very simple example:
def outerFunc(number: Int) = {
def inner(original: Int) = original * 2
inner(number) + inner(number + 1)
}
inner
is defined within the other function’s body and is meant
to be used only within the containing function.Companion objects
As an exception to the general rules outlined above, a class and its companion object have access to each other’s private members. Here’s a summary of an example from Chapter 5.3:
object Customer {
private var createdInstanceCount = 0
}
class Customer(val name: String) {
Customer.createdInstanceCount += 1
val number = Customer.createdInstanceCount
override def toString = "#" + this.number + " " + name
}
createdInstanceCount
exists in memory, since the customer
object is a singleton. This contrasts with the names and
numbers of the various Customer
instances.Customer
class and its companion object are “friends”
and have access to each other’s private members.Pairs and Other Tuples
A tuple is an immutable structure that consists of two or more values that may or may not have the same data type (Chapter 8.4). You can use round brackets and commas to define a tuple:
val quartet = ("This tuple has four members of different types.", 100, 3.14159, false)quartet: (String, Int, Double, Boolean) = (This tuple has four members of different types.,100,3.14159,false)
A tuple’s members are numbered from one(!) upward. You access them via names that start with an underscore:
quartet._1res59: String = This tuple has four members of different types. quartet._3res60: Double = 3.14159
Pairs are tuples with two members. Both members of this pair are strings:
val pair = ("laama", "llama")pair: (String, String) = (laama,llama)
You can assign the members of a pair to multiple variables with a single command:
val (finnish, english) = pairfinnish: String = laama english: String = llama
Instead of the brackets and the comma, you can define a pair like this:
val identicalPair = "laama" -> "llama"identicalPair: (String, String) = (laama,llama)
The latter notation is particularly popular when using pairs to store the keys and
values of a Map
; see Map
s, below.
More about Strings
Methods on string objects
This section lists examples of selected methods on String
s (Chapters 3.3 and 5.2).
Strings are introduced above at Characters and strings; for still more methods,
see the sections Collection Basics and Processing Collections with Higher-Order
Methods further down on this page (since strings are collections, too).
There are two ways to check a string’s length (size):
val myString = "Olavi Eerikinpoika Stålarm"myString: String = Olavi Eerikinpoika Stålarm myString.lengthres61: Int = 26 myString.sizeres62: Int = 26
Changing letter case:
"five hours of Coding can save 15 minutes of Planning".toUpperCaseres63: String = FIVE HOURS OF CODING CAN SAVE 15 MINUTES OF PLANNING "five hours of Coding can save 15 minutes of Planning".toLowerCaseres64: String = five hours of coding can save 15 minutes of planning "five hours of Coding can save 15 minutes of Planning".capitalizeres65: String = Five hours of Coding can save 15 minutes of Planning
Selecting a part of a string:
"Olavi Eerikinpoika Stålarm".substring(6, 11)res66: String = Eerik "Olavi Eerikinpoika Stålarm".substring(3)res67: String = vi Eerikinpoika Stålarm
Splitting a string:
"Olavi Eerikinpoika Stålarm".split(" ")res68: Array[String] = Array(Olavi, Eerikinpoika, Stålarm) "Olavi Eerikinpoika Stålarm".split("la")res69: Array[String] = Array(O, vi Eerikinpoika Stå, rm)
Removing leading and trailing whitepace:
val myText = " whitespace trimmed from around the string but not the middle "myText: String = " whitespace trimmed from around the string but not the middle " myText.trimres70: String = whitespace trimmed from around the string but not the middle
Interpreting the characters in a string as a number:
"100".toIntres71: Int = 100 "100".toDoubleres72: Double = 100.0 "100.99".toDoubleres73: Double = 100.99 "one hundred".toIntjava.lang.NumberFormatException: For input string: "one hundred" ... " 100".toIntjava.lang.NumberFormatException: For input string: " 100" ... " 100".trim.toIntres74: Int = 100
You can do the above more safely with the Option
-suffixed methods:
"100".toIntOptionres75: Option[Int] = Some(100) "one hundred".toIntOptionres76: Option[Int] = None "100.99".toDoubleOptionres77: Option[Double] = Some(100.99)
Comparing strings by the Unicode alphabet:
"abc" < "bcd"res78: Boolean = true "abc" >= "bcd"res79: Boolean = false "abc".compare("bcd")res80: Int = -1 "bcd".compare("abc")res81: Int = 1 "abc".compare("abc")res82: Int = 0 "abc".compare("ABC")res83: Int = 32 "abc".compareToIgnoreCase("ABC")res84: Int = 0
Embedding values in a string
You can embed any expression’s value in a string (Chapter 1.4).
val number = 100number: Int = 100 val stringWithEmbeddedValues = s"The variable stores $number, which is slightly less than ${number + 1}."stringWithEmbeddedValues: String = The variable stores 100, which is slightly less than 101.
s
.You can use the plus operator to combine a string with values of different types, such
as Int
s:
val theSameUsingPlus = "The variable stores " + number + ", which is slightly less than " + (number + 1) + "."theSameUsingPlus: String = The variable stores 100, which is slightly less than 101. "the number is " + numberres85: String = the number is 100 "kit" + 10res86: String = kit10
Those examples appended values to the ends of strings. However, the other way around — with the number before the plus — isn’t okay:
number + " is the number"number + " is the number" ^ warning: method + in class Double is deprecated (since 2.13.0): Adding a number and a String is deprecated. Use the string interpolation `s"$num$str"`
Special characters in strings
A backslash character marks a special character within a string (Chapter 5.2):
val newline = "\n"newline: String = " " println("first row\nsecond row")first row second row val tabulator = "first\tsecond\tthird"tabulator: String = first second third "here's a double quotation mark \" and another \""res87: String = here's a double quotation mark " and another " "here's a backslash \\ and another \\"res88: String = here's a backslash \ and another \
If you triple the double quotes around a string literal, you can write special characters without “escaping” them with the backslash:
"""This string contains a quotation mark " and a backslash \ on two separate rows."""res89: String = This string contains a quotation mark " and a backslash \ on two separate rows.
The toString
method
All Scala objects have a parameterless method named toString
. It returns a description
of the object as a string:
100.toStringres90: String = 100 false.toStringres91: String = false
All custom classes and objects that you write have a toString
method, too
(because they inherit it; see Inheritance, below):
class MyClass(val variable: Int)defined class MyClass val myObj = new MyClass(10)myObj: MyClass = MyClass@56181 myObj.toStringres92: String = MyClass@56181 myObjres93: MyClass = MyClass@56181
toString
method generates strings that look like
this (Chapter 2.5)toString
as it describes objects. What you see
above is three outputs obtained by calling toString
thrice.You can override the default implementation of toString
(see Chapter 2.5 and
Inheritance, below):
class Experiment(val value: Int) { override def toString = "THE OBJECT'S VALUE IS " + this.value }defined class Experiment val testObj = new Experiment(11)testObj: Experiment = THE OBJECT'S VALUE IS 11
toString
also gets called whenever we print out an object or combine an object with a
string:
println(testObj)THE OBJECT'S VALUE IS 11 testObj + "!!!"res94: String = THE OBJECT'S VALUE IS 11!!! s"testObj's toString returns something that we embed here $testObj in the middle of this string."res95: String = testObj's toString returns something that we embed here THE OBJECT'S VALUE IS 11 in the middle of this string.
Collection Basics
Basic use of a buffer
Buffers are a type of collection (Chapters 1.5 and 4.2). The corresponding type Buffer
is in scala.collection.mutable
:
import scala.collection.mutable.Bufferimport scala.collection.mutable.Buffer
Creating a buffer:
Buffer("first", "second", "third", "and a fourth")res96: Buffer[String] = ArrayBuffer(first, second, third, and a fourth)
val numbers = Buffer(12, 2, 4, 7, 4, 4, 10, 3)numbers: Buffer[Int] = ArrayBuffer(12, 2, 4, 7, 4, 4, 10, 3)
new
when you
create one (because we instantiate buffers with a factory method).A buffer may be empty:
val youCanAddNumbersHere = Buffer[Double]()youCanAddNumbersHere: Buffer[Double] = ArrayBuffer()
A buffer contains zero or more elements, stored in order, each at its own index. Indices run from zero(!) upwards.
Here’s how to look up a single element, given its index:
numbers(0)res97: Int = 12 numbers(3)res98: Int = 7
The above are actually shorthand expressions for calling the buffer’s apply
method
(Chapter 5.3):
numbers.apply(0)res99: Int = 12 numbers.apply(3)res100: Int = 7
The lift
method similarly accesses a buffer element. However, lift
returns the
result in an Option
and doesn’t crash at runtime if the index is invalid:
numbers(10000)java.lang.IndexOutOfBoundsException: 10000 ... numbers.lift(10000)res101: Option[Int] = None numbers.lift(-1)res102: Option[Int] = None numbers.lift(3)res103: Option[Int] = Some(7)
You can replace a buffer element with another:
numbers(3) = 1val theFourthElementIsNow = numbers(3)theFourthElementIsNow: Int = 1
The operator +=
adds a single element at the end of the buffer, thus increasing the
buffer’s size:
numbers += 11res104: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3, 11) numbers += -50res105: Buffer[Int] = ArrayBuffer(12, 2, 4, 1, 4, 4, 10, 3, 11, -50)
The operator -=
removes an element:
numbers -= 4res106: Buffer[Int] = ArrayBuffer(12, 2, 1, 4, 4, 10, 3, 11, -50) numbers -= 4res107: Buffer[Int] = ArrayBuffer(12, 2, 1, 4, 10, 3, 11, -50)
Here are some additional commands for adding and removing elements:
numbers.append(100)numbers.prepend(1000)numbersres108: Buffer[Int] = ArrayBuffer(1000, 12, 2, 1, 4, 10, 3, 11, -50, 100) numbers.insert(5, 50000)numbersres109: Buffer[Int] = ArrayBuffer(1000, 12, 2, 1, 4, 50000, 10, 3, 11, -50, 100) val removedFourthElement = numbers.remove(3)removedFourthElement: Int = 1 numbersres110: Buffer[Int] = ArrayBuffer(1000, 12, 2, 4, 50000, 10, 3, 11, -50, 100)
Collection types: buffers, vectors, lazy-lists, etc.
There are many types of collections. In O1, we first use mostly buffers, then increasingly turn to vectors. We eventually run into several other collection types, too.
Both buffers and vectors store elements in a specific order, each at its own index. The most obvious differences between the types are these:
- A buffer is a mutable collection. You can add elements to a buffer, changing its size. You can also remove elements or replace them with new ones.
- A vector is an immutable collection. When you create a vector, you specify any and all elements that will ever be in that vector. A vector’s element is never replaced by another; a vector’s size never changes.
You use a Vector
much like you use a Buffer
(shown above), except that you can’t
change what’s in an existing vector. You also don’t need to an import
command;
Vector
s are always available.
Here are a couple of examples:
val myVector = Vector(12, 2, 4, 7, 4, 4, 10, 3)myVector: Vector[Int] = Vector(12, 2, 4, 7, 4, 4, 10, 3) myVector(6)res111: Int = 10 myVector.lift(10000)res112: Option[Int] = None
More collections:
- Strings are collections of elements. More on that in the next section below.
Range
s are collections that represent ranges of numbers. See below for examples.- An
Array
is a basic, numerically indexed data structure. Each array has a fixed size (like a vector) but its elements can be replaced by new ones (like a buffer’s). In Scala, usingArray
s is near-identical to using vectors and buffers (Chapter 11.1). List
s are collections that work particularly well when the elements are processed in order. See Chapter 11.2 for a brief introduction.LazyList
s are similar to “regular”List
s. Their special power is that a lazy-list’s elements are constructed and stored in memory only when or if necessary. For more on lazy-lists, see the separate section further down on this page or Chapter 7.1.- A
Set
may only ever contain a single copy of each element (Chapter 9.2). The elements of a set aren’t ordered in the same sense as the other collections listed above. - A
Map
is not indexed numerically but by key. Maps have a dedicated section on this page. - Stacks follow the LIFO principle: whichever element was added last is removed first (Chapter 11.2).
- An
Option
is a collection with no more than a single element.
Factors such as readability and efficiency influence the choice of collection type; different collections are popular in different programming paradigms.
The official Scala documentation contains a compact summary of the available collection classes.
Collections may be nested: one collection may store references to other collections. See Chapter 6.1 for a discussion.
Strings as collections
A string is a collection (Chapters 5.2 and 5.6). You can work on a string much like
you work on a vector. The elements of a String
are Char
s.
val myString = "llama"myString: String = llama myString(3)res113: Char = m myString.lift(3)res114: Option[Char] = Some(m)
Regular strings of type String
are immutable. For instance, concatenating two strings
generates a new, combined string rather than changing either of the originals. (Mutable
representations of strings are possible, too; see Chapter 10.2).
Range
s of numbers
A Range
object is an immutable collection that represents numbers within a specified
interval (Chapters 5.2 and 5.6).
val fourToTen = Range(4, 11)fourToTen: Range = Range 4 until 11 fourToTen(0)res115: Int = 4 fourToTen(2)res116: Int = 6
You can also construct a Range
by calling until
or to
on an Int
(Chapter 5.2).
The latter method includes the given end point in the resulting Range
. For example,
these two commands produce a seven-number range identical to the one above:
val anIndenticalRange = 4 until 11anIndenticalRange: Range = Range 4 until 11 val alsoIdentical = 4 to 10alsoIdentical: Range = Range 4 to 10
You don’t have to include every consecutive integer in the Range
; you can skip some
systematically:
val everyOtherInt = 1 to 10 by 2everyOtherInt: Range = Range 1 to 10 by 2 val everyThirdInt = 1 to 10 by 3everyThirdInt: Range = Range 1 to 10 by 3
Common Methods on Collections
This section complements the above introduction to collections by listing various general-purpose methods on Scala collections. All the methods listed here are first-order methods; you’ll find more tools further down at Processing Collections with Higher-Order Methods.
The examples in this section use strings and vectors to exemplify collections. However,
all the methods in the examples are similarly available on buffers, arrays, and various
other collection types. Some of them are also available on collections that don’t have
numerical indices, such as Map
s.
Checking size: size
, isEmpty
, and nonEmpty
Methods for examining the number of elements in a collection (Chapter 4.2):
Vector(10, 100, 100, -20).sizeres117: Int = 4 Vector().sizeres118: Int = 0 Vector(10, 100, 100, -20).isEmptyres119: Boolean = false Vector(10, 100, 100, -20).nonEmptyres120: Boolean = true Vector().isEmptyres121: Boolean = true Vector().nonEmptyres122: Boolean = false "llama".isEmptyres123: Boolean = false "".isEmptyres124: Boolean = true
Element lookup: contains
and indexOf
Methods for determining if a given element exists in a collection and, if so, where (Chapter 4.2):
val containsElementM = "llama mmama".contains('m')containsElementM: Boolean = true val containsElementZ = "llama mmama".contains('z')containsElementZ: Boolean = false val indexOfFirstA = "llama mmama".indexOf('a')indexOfFirstA: Int = 2 val similarOperationOnVector = Vector(10, 100, 100, -20).indexOf(-20)similarOperationOnVector: Int = 3 val negativeMeansNotFound = "llama mmama".indexOf('z')negativeMeansNotFound: Int = -1 val searchFromGivenIndexOnward3 = "llama mmama".indexOf('a', 4)searchFromGivenIndexOnward3: Int = 8 val searchBackwards = "llama mmama".lastIndexOf('a')searchBackwards: Int = 10
Parts of a collection: head
, tail
, take
, drop
, slice
, etc.
There are many ways to select one or more of the first elements in a collection (Chapters 4.2 and 5.2):
val firstElem = "llama".headfirstElem: Char = l val noFirstElementSoThisFails = "".headjava.util.NoSuchElementException: next on empty iterator ... val firstWrapped = "llama".headOptionfirstWrapped: Option[Char] = Some(l) val firstMissing = "".headOptionfirstMissing: Option[Char] = None val firstThreeElems = "llama".take(3)firstThreeElems: String = lla val tooMuchButNoProb = "llama".take(1000)tooMuchButNoProb: String = llama val allButLast = "llama".initallButLast: String = llam val allButLastThree = "llama".dropRight(3)allButLastThree: String = ll val worksOnDifferentCollections = Vector(10, 100, 100, -20).dropRight(2)worksOnDifferentCollections: Vector[Int] = Vector(10, 100)
None of these methods modifies the original collection. They create new collections that contain some of the elements of the originals. The same goes for the commands below, which select elements from the rear end of a collection:
val allButFirst = "llama".tailallButFirst: String = lama val allButFirstThree = "llama".drop(3)allButFirstThree: String = ma val lastOnly = "llama".lastlastOnly: Char = a val lastWrapped = "llama".lastOptionlastWrapped: Option[Char] = Some(a) val lastThree = "llama".takeRight(3)lastThree: String = mma
Cutting a string in two with splitAt
(Chapter 8.4):
val myText = "llama/mmama"myText: String = llama/mmama val pairOfPieces = myText.splitAt(6)pairOfPieces: (String, String) = (llama,/mmama) val sameButLonger = (myText.take(6), myText.drop(6))sameButLonger: (String, String) = (llama,/mmama)
Selecting a slice
of a collection:
Vector("first/0", "second/1", "third/2", "fourth/3", "fifth/4").slice(1, 4)res125: Vector[String] = Vector(second/1, third/2, fourth/3)
The element at the start index is included. The element at the end index isn’t.
Adding elements and combining collections
You can form a new collection by adding elements:
val numbers = Vector(10, 20, 100, 10, 50, 20)numbers: Vector[Int] = Vector(10, 20, 100, 10, 50, 20) val oneMoreAppended = numbers :+ 999999oneMoreAppended: Vector[Int] = Vector(10, 20, 100, 10, 50, 20, 999999) val oneMorePrepended = 999999 +: numbersoneMorePrepended: Vector[Int] = Vector(999999, 10, 20, 100, 10, 50, 20) val combinedCollection = numbers ++ Vector(999, 998, 997)combinedCollection: Vector[Int] = Vector(10, 20, 100, 10, 50, 20, 999, 998, 997)
Adding elements like this, by constructing new collections, is possible also when the collection is immutable (as above). For examples of modifying an existing mutable collection, see the earlier section Basic use of a buffer.
A mnemonic for collection operators (like +:
)
+:
operator the
wrong way around 🤦🏼♀️.Here’s a Scala mnemonic:
The COLon goes on the COLlection side.
That is, these are fine:
myVector :+ newElem // appends an element
newElem +: myVector // prepends an element
But these aren’t:
myVector +: newElem // error
newElem :+ myVector // error
Copying elements in a new collection: to
, toVector
, toSet
, etc.
You can switch between collection types by copying elements from one collection to a new one (Chapter 4.2):
val myVector = "llama".toVectormyVector: Vector[Char] = Vector(l, l, a, m, a) val myBuffer = myVector.toBuffermyBuffer: Buffer[Char] = ArrayBuffer(l, l, a, m, a) val myArray = myBuffer.toArraymyArray: Array[Char] = Array(l, l, a, m, a) val mySet = "happy llama".toSetmySet: Set[Char] = Set(y, a, m, , l, p, h) val anotherVector = myArray.to(Vector))anotherVector: Vector[Char] = Vector(l, l, a, m, a) val myLazyList = myArray.to(LazyList)myLazyList: LazyList[Char] = LazyList(<not computed>)
toVector
or toBuffer
for many
collection types (but not all).to
takes a parameter that specifies
the type of the target collection.newBuilder
and another way of initializing a collection
Sometimes it’s desirable to gather elements one or more at a time and, once done, fix the construct a collection (e.g., a vector) from the gathered elements. To gather the elements, it often helps to have a temporary, mutable helper object that stores the elements that have already been dealt with.
The Scala API provides objects called Builder
s that can be used
for just such a purpose. Many collection types come with a newBuilder
method that creates a suitable and efficient Builder
object. Here’s
an example of constructing a Vector
:
val collectedSoFar = Vector.newBuilder[String]collectedSoFar: ReusableBuilder[String,Vector[String]] = VectorBuilder@5dcf0772 collectedSoFar += "first"collectedSoFar += "second"collectedSoFar += "third"val finalCollection = collectedSoFar.result()finalCollection: Vector[String] = Vector(first, second, third)
Miscellaneous methods: mkString
, indices
, zip
, reverse
, flatten
, etc.
The mkString
method formats elements as a string (Chapter 4.2):
val myVector = Vector(100, 20, 30)myVector: Vector[Int] = Vector(100, 20, 30) println(myVector.toString)Vector(100, 20, 30) println(myVector)Vector(100, 20, 30) println(myVector.mkString)1002030 println(myVector.mkString("---"))100---20---30
It’s easy to get all the indices of a collection as a Range
(Chapter 5.6):
"laama".indicesres126: Range = Range 0 until 5 Vector(100, 20, 30).indicesres127: Range = Range 0 until 3
You can zip
two collections into a collection of pairs (Chapter 8.4):
val species = Vector("llama", "alpaca", "vicuña")species: Vector[String] = Vector(llama, alpaca, vicuña) val heights = Vector(180, 80, 60)heights: Vector[Int] = Vector(180, 80, 60) val heightsAndSpecies = heights.zip(species)heightsAndSpecies: Vector[(Int, String)] = Vector((180,llama), (80,alpaca), (60,vicuña)) val threePairsSinceOnlyThreeHeights = heights.zip(Vector("llama", "alpaca", "vicuña", "guanaco"))threePairsSinceOnlyThreeHeights: Vector[(Int, String)] = Vector((180,llama), (80,alpaca), (60,vicuña)) val vectorOfPairsIntoPairOfVectors = heightsAndSpecies.unzipvectorOfPairsIntoPairOfVectors: (Vector[Int], Vector[String]) = (Vector(180, 80, 60), Vector(llama, alpaca, vicuña)) val speciesAndIndices = species.zip(species.indices)speciesAndIndices: Vector[(String, Int)] = Vector((llama,0), (alpaca,1), (vicuña,2)) val theSameThing = species.zipWithIndextheSameThing: Vector[(String, Int)] = Vector((llama,0), (alpaca,1), (vicuña,2))
The reverse
of a collection has the same elements backwards (Chapter 4.2):
"llama".reverseres128: String = amall Vector(10, 20, 15).reverseres129: Vector[Int] = Vector(15, 20, 10)
A nested collection can be flatten
ed (Chapter 6.1):
val twoDimensional = Vector(Vector(1, 2), Vector(100, 200), Vector(2000, 1000))twoDimensional: Vector[Vector[Int]] = Vector(Vector(1, 2), Vector(100, 200), Vector(2000, 1000)) val oneDimensional = twoDimensional.flattenoneDimensional: Vector[Int] = Vector(1, 2, 100, 200, 2000, 1000)
See the Scala API documentation
for many more miscellaneous methods such as sum
, product
, grouped
, sliding
, transpose
,
etc. Collections also have various powerful higher-order methods; see Processing Collections with
Higher-Order Methods, below.
More on Functions
Higher-order functions
You can pass a function as a parameter to another function; below is a summary of an example from Chapter 6.1.
twice
is a higher-order function:
def twice(operation: Int => Int, target: Int) = operation(operation(target))
twice
, the first parameter must be a function that
takes in an integer and also returns an integer. The variable
operation
will then store a reference to that function.twice
calls its parameter function, takes the return value, and
then calls the parameter again on that value.Here are a couple of ordinary functions that work in combination with twice
:
def next(number: Int) = number + 1
def doubled(original: Int) = 2 * original
Usage examples:
twice(next, 1000)res130: Int = 1002 twice(doubled, 1000)res131: Int = 4000
Function literals and anonymous functions
Instead of defining a function with def
, you can write the function as a
literal. A function literal defines an anonymous function (Chapter 6.2).
As an example, let’s use this higher-order function:
def twice(operation: Int => Int, target: Int) = operation(operation(target))
twice(number => number + 1, 1000)res132: Int = 1002
twice(n => 2 * n, 1000)res133: Int = 4000
twice
a reference to this anonymous function.(number: Int) => number + 1
, but the longer
form is unnecessary here, because the fact that the parameter is an
Int
can be automatically inferred from the context.Here’s another example of a higher-order function (from Chapters 6.1 and 6.2):
def areSorted(first: String, second: String, third: String, compare: (String, String) => Int) =
compare(first, second) <= 0 && compare(second, third) <= 0
areSorted
’s last parameter is a function that takes in two
strings and returns an integer.A couple of usage examples:
val areSortedByLength = areSorted("Java", "Scala", "Haskell", (j1, j2) => j1.length - j2.length)areSortedByLength: Boolean = true val areSortedByUnicode = areSorted("Java", "Scala", "Haskell", (j1, j2) => j1.compare(j2))areSortedByUnicode: Boolean = false
Shorter function literals: anonymous parameters
Instead of naming the parameters in a function literal and using the rightward arrow, you can often write a more compact literal by using an underscore to mark unnamed parameters (Chapter 6.2). These two code fragments are equivalent:
twice(number => number + 1, 1000)
twice(n => 2 * n, 1000)
twice( _ + 1 , 1000)
twice( 2 * _ , 1000)
As are these two:
areSorted("Java", "Scala", "Haskell", (j1, j2) => j1.length - j2.length )
areSorted("Java", "Scala", "Haskell", (j1, j2) => j1.compare(j2) )
areSorted("Java", "Scala", "Haskell", _.length - _.length )
areSorted("Java", "Scala", "Haskell", _.compare(_) )
The compact notation works only in cases that are sufficiently simple. One restriction is that each anonymous parameter (underscore) can be used only once in the function body. You may also need to use the longer notation if the function literal contains further function calls. For more details, please see Chapter 6.2.
Processing Collections with Higher-Order Methods
Collections have many powerful higher-order methods that take in a function an apply it to the collection’s elements (Chapters 6.3, 6.4, 9.2, and 9.3). This section lists some of them. The examples use strings and vectors, but the same methods are available on other collections as well.
Repeating an operation: foreach
The foreach
method performs an effect on each element of the collection (Chapter 6.3):
Vector(10, 50, 20).foreach(println)10
50
20
"llama".foreach( letter => println(letter.toUpper + "!") )L!
L!
A!
M!
A!
Turning elements into something else: map
, flatMap
The map
method generates a collection whose elements are computed from those in the
original collection as per the given parameter function (Chapter 6.3):
val words = Vector("Witness", "Opener", "Candy")words: Vector[String] = Vector(Witness, Opener, Candy) words.map( word => "i" + word )res134: Vector[String] = Vector(iWitness, iOpener, iCandy) words.map( word => word.length )res135: Vector[Int] = Vector(7, 6, 5)
Here’s the same with compact function literals:
words.map( "i" + _ )res136: Vector[String] = Vector(iWitness, iOpener, iCandy) words.map( _.length )res137: Vector[Int] = Vector(7, 6, 5)
If map
’s parameter function returns a collection, you get a nested structure:
val numbers = Vector(100, 200, 150)numbers: Vector[Int] = Vector(100, 200, 150) numbers.map( number => Vector(number, number + 1) )res138: Vector[Vector[Int]] = Vector(Vector(100, 101), Vector(200, 201), Vector(150, 151))
flatMap
does the same as map
and flatten
combined. It produces a “flatter”
collection than map
does (Chapter 6.3):
numbers.flatMap( number => Vector(number, number + 1) )res139: Vector[Int] = Vector(100, 101, 200, 201, 150, 151)
The properties of collection elements: exists
, forall
, filter
, takeWhile
, etc.
exists
finds out whether a given criterion is true for even a single element in the
collection (Chapter 6.3); forall
similarly works out whether a criterion is true
for all the elements of the collection; count
computes the number of elements that
meet a criterion:
val numbers = Vector(10, 5, 4, 5, -20)numbers: Vector[Int] = Vector(10, 5, 4, 5, -20) numbers.exists( _ < 0 )res140: Boolean = true numbers.exists( _ < -100 )res141: Boolean = false numbers.forall( _ > 0 )res142: Boolean = false numbers.forall( _ > -100 )res143: Boolean = true numbers.count( _ > 0 )res144: Int = 4
find
locates the first element that meets a given criterion (Chapter 6.3); indexWhere
does the same but returns an index rather than the element itself (Chapter 6.4):
val numbers = Vector(10, 5, 4, 5, -20)numbers: Vector[Int] = Vector(10, 5, 4, 5, -20) numbers.find( _ < 5 )res145: Option[Int] = Some(4) numbers.find( _ == 100 )res146: Option[Int] = None numbers.indexWhere( _ < 5 )res147: Int = 2 numbers.indexWhere( _ == 100 )res148: Int = -1
filter
returns all the elements that meet a criterion (Chapter 6.3); filterNot
does
the inverse of that; partition
splits the elements in those that meet the criterion and
those that don’t:
val numbers = Vector(10, 5, 4, 5, -20)numbers: Vector[Int] = Vector(10, 5, 4, 5, -20) val atLeastFive = numbers.filter( _ >= 5 )atLeastFive: Vector[Int] = Vector(10, 5, 5) val underFive = numbers.filterNot( _ >= 5 )underFive: Vector[Int] = Vector(4, -20) val thoseTwoAsAPair = numbers.partition( _ >= 5 )thoseTwoAsAPair: (Vector[Int], Vector[Int]) = (Vector(10, 5, 5),Vector(4, -20))
takeWhile
keeps taking elements until it finds an element that meets the given criterion
(Chapter 6.3); dropWhile
takes exactly the elements that takeWhile
doesn’t; span
does
both things at once:
val numbers = Vector(10, 5, 4, 5, -20)numbers: Vector[Int] = Vector(10, 5, 4, 5, -20) val untilSmallEnough = numbers.takeWhile( _ >= 5 )untilSmallEnough: Vector[Int] = Vector(10, 5) val firstSmallOnwards = numbers.dropWhile( _ >= 5 )firstSmallOnwards: Vector[Int] = Vector(4, 5, -20) val bothAsAPair = numbers.span( _ >= 5 )bothAsAPair: (Vector[Int], Vector[Int]) = (Vector(10, 5),Vector(4, 5, -20))
Relative order of elements: maxBy
, minBy
, sortBy
The methods maxBy
and minBy
search for the collection’s largest or smallest element,
using a given criterion (Chapter 9.2); sortBy
formes a fully sorted
version of the collection:
import scala.math.absimport scala.math.abs val numbers = Vector(10, 5, 4, 5, -20)numbers: Vector[Int] = Vector(10, 5, 4, 5, -20) val largestAbs = numbers.maxBy(abs)largestAbs: Int = -20 val smallestAbs = numbers.minBy(abs)smallestAbs: Int = 4 val sortedByAbs = numbers.sortBy(abs)sortedByAbs: Vector[Int] = Vector(4, 5, 5, 10, -20) val words = Vector("the longest of them all", "short", "middling-sized", "shortish")words: Vector[String] = Vector(the longest of them all, short, middling-sized, shortish) val longest = words.maxBy( _.length )longest: String = the longest of them all val sortedByLength = words.sortBy( _.length )sortedByLength: Vector[String] = Vector(short, shortish, middling-sized, the longest of them all)
Looking for the maximal or minimal element fails in case there are no elements at
all. A convenient way to deal with that special case is to use the maxByOption
or
minByOption
:
words.maxByOption( _.length )res149: Option[String] = Some(the longest of them all) words.minByOption( _.length )res150: Option[String] = Some(short) words.drop(100).minByOption( _.length )res151: Option[String] = None
The above methods have variants named max
, min
, sorted
, maxOption
, and minOption
,
respectively. These By
-less methods require that the elements have a natural ordering
and base their behavior on that (Chapter 9.2). Here are some examples of natural sorting:
val ascendingNumbers = numbers.sortedascendingNumbers: Vector[Int] = Vector(-20, 4, 5, 5, 10) val sortedByUnicode = words.sortedsortedByUnicode: Vector[String] = Vector(middling-sized, short, shortish, the longest of them all) val theSameThing = words.sortBy( sana => sana )theSameThing: Vector[String] = Vector(middling-sized, short, shortish, the longest of them all) val alsoTheSame = words.sortBy(identity)alsoTheSame: Vector[String] = Vector(middling-sized, short, shortish, the longest of them all) val sortedLetters = "Let's offroad!".sortedsortedLetters: String = " !'Ladeffoorst"
If the elements to be sorted or compared are Double
s, you need to spefify how to order
them. There are two standard ways of doing that: TotalOrdering
and IeeeOrdering
,
either of which works fine for most purposes. (For more details, see the API docs.)
import scala.Ordering.Double.TotalOrderingimport scala.Ordering.Double.TotalOrdering Vector(1.1, 3.0, 0.0, 2.2).sortedres152: Vector[Double] = Vector(0.0, 1.1, 2.2, 3.0) Vector(1.1, 3.0, 0.0, 2.2).maxres153: Double = 3.0 Vector(-10.0, 1.5, 9.5).maxBy( _.abs )res154: Double = -10.0
Generic processing of elements: foldLeft
and reduceLeft
The methods foldLeft
and reduceLeft
work at a slightly lower level of abstraction:
you define precisely how to process each element in turn in order to construct a return
value (Chapter 6.4). First, here’s foldLeft
:
val numbers = Vector(10, 5, 4, 5, -20)numbers: Vector[Int] = Vector(10, 5, 4, 5, -20) val sum = numbers.foldLeft(0)( (sumSoFar, next) => sumSoFar + next )sum: Int = 4 val sameThing = numbers.foldLeft(0)( _ + _ )sameThing: Int = 4
reduceLeft
is similar, but it uses the first element as the initial value and thus
needs only the function as a parameter:
import scala.math.minimport scala.math.min val numbers = Vector(10, 5, 4, 5, -20)numbers: Vector[Int] = Vector(10, 5, 4, 5, -20) val sum = numbers.reduceLeft( _ + _ )sum: Int = 4 val smallest = numbers.reduceLeft(min)smallest: Int = -20
The return value of reduceLeft
shares its type with the elements of the collection,
but foldLeft
can generate a result of a different type:
val bigNumberExists = numbers.foldLeft(false)( (foundYet, next) => foundYet || next > 10000 )bigNumberExists: Boolean = false
Since reduceLeft
assumes that the collection has at least one element, it crashes at
runtime is the assumption is not met:
val empty = Vector[Int]()empty: Vector[Int] = Vector() empty.foldLeft(0)( _ + _ )res155: Int = 0 empty.reduceLeft( _ + _ )java.lang.UnsupportedOperationException: empty.reduceLeft ...
reduceLeftOption
is like reduceLeft
but doesn’t crash on an empty input. It returns
the result in an Option
wrapper:
empty.reduceLeftOption( _ + _ )res156: Option[Int] = None
For more collection methods, see the Scala API documentation.
Option
as a collection
Option
is a kind of collection: every Option
has either a single element (Some
) or
zero elements (None
). See Chapter 8.2 for a discussion. Below is a list of examples of
collection methods applied to Option
s.
The examples use these two variables:
val something: Option[Int] = Some(100)something: Option[Int] = Some(100) val nothing: Option[Int] = Nonenothing: Option[Int] = None
size
:
something.sizeres157: Int = 1 nothing.sizeres158: Int = 0
foreach
:
something.foreach(println)100 nothing.foreach(println) // Doesn't print anything.
contains
:
something.contains(100)res159: Boolean = true something.contains(50)res160: Boolean = false nothing.contains(100)res161: Boolean = false
exists
:
something.exists( _ > 0 )res162: Boolean = true something.exists( _ < 0 )res163: Boolean = false nothing.exists( _ > 0 )res164: Boolean = false
forall
:
something.forall( _ > 0 )res165: Boolean = true something.forall( _ < 0 )res166: Boolean = false nothing.forall( _ > 0 )res167: Boolean = true
filter
:
something.filter( _ > 0 )res168: Option[Int] = Some(100) something.filter( _ < 0 )res169: Option[Int] = None nothing.filter( _ > 0 )res170: Option[Int] = None
map
:
something.map( 2 * scala.math.Pi * _ )res171: Option[Double] = Some(628.3185307179587) nothing.map( 2 * scala.math.Pi * _ )res172: Option[Double] = None
flatten
:
Some(something)res173: Some[Option[Int]] = Some(Some(100)) Some(nothing)res174: Some[Option[Int]] = Some(None) Some(something).flattenres175: Option[Int] = Some(100) Some(nothing).flattenres176: Option[Int] = None
flatMap
:
def myFunc(number: Int) = if (number != 0) Some(1000 / number) else NonemyFunc: (number: Int)Option[Int] something.flatMap(myFunc)res177: Option[Int] = Some(10) Some(0).flatMap(myFunc)res178: Option[Int] = None nothing.flatMap(myFunc)res179: Option[Int] = None
Creating elements with a function: tabulate
Scala’s collection types come with a factory method named tabulate
that creates
collections by using a given “formula” to initialize each element (Chapters 6.1 and 6.2).
This method takes two parameter lists. The first indicates the number of elements — the size of the collection to be created. The second supplies a function that is called on each index to create the corresponding element:
Vector.tabulate(10)( index => index * 2 )res180: Vector[Int] = Vector(0, 2, 4, 6, 8, 10, 12, 14, 16, 18)
tabulate
repeatedly calls the function it receives, passing
in each index in turn. Here, a doubling function has been called
on each of the numbers from 0 to 9.You can do the same in more than one dimension:
Vector.tabulate(3, 4)( (first, second) => first * 100 + second )res181: Vector[Vector[Int]] = Vector(Vector(0, 1, 2, 3), Vector(100, 101, 102, 103), Vector(200, 201, 202, 203))
Lazy-Lists and Related Topics
The LazyList
class
A lazy-list is a collection whose elements are generated and stored only when needed, lazily (Chapter 7.1). It’s designed for processing the needed elements in order. You can operate on a lazy-list’s elements one by one without storing them all in memory simultaneously.
In many respects, a lazy-list is just like the other collection types described above. For instance, it’s possible to create a lazy-list by typing in all its elements or by copying the contents of an existing collection:
val myLazyData = LazyList(10.2, 32.1, 3.14159)myLazyData: LazyList[Double] = LazyList(<not computed>) myLazyData.mkString(" ")res182: String = 10.2 32.1 3.14159 val vectorOfWords = Vector("first", "second", "third", "fourth")vectorOfWords: Vector[String] = Vector(first, second, third, fourth) val lazyWords = vectorOfWords.to(LazyList)lazyWords: LazyList[String] = LazyList(<not computed>)
LazyList
s have many familiar methods. Here are just a few:
lazyWords.drop(2).headres183: String = third lazyWords.filter( _.length > 4 ).map( _ + "!" ).foreach(println)third! fourth!
In the examples above, the lazy-lists were finite. However, unlike the other collections,
a lazy-lists may also be infinite. One way to create an infinite lazy-list is the
continually
factory method:
val myLazyStrings = LazyList.continually("Oliver")myLazyStrings: LazyList[String] = LazyList(<not computed>) myLazyStrings.take(5).foreach(println)Oliver Oliver Oliver Oliver Oliver
"Oliver"
(repeating that evaluation
whenever it must). Since, in this example, that expression is a
literal, all of this collection’s elements are identical.take
returns a sublist
of a specified size.An infinite lazy-list may also have elements that differ from each other:
LazyList.continually( Random.nextInt(100) ).takeWhile( _ <= 90 ).mkString(",")res184: String = 0,65,83,38,75,33,11,18,75,51,3
takeWhile
method is still not enough to make
the lazy-list generate the random numbers; it just makes a
LazyList
object that is capable of generating them up to
a point.mkString
uses all the elements of the list to construct
a string. This forces the LazyList
object to evaluate the
number-generating expression repeatedly.One way to structure an interactive program is to use a lazy-list. The following example
from Chapter 7.1 prompts the user for input until they say "please"
and reports
the length of each input as shown in this example run:
Enter some text: hello The input is 5 characters long. Enter some text: stop The input is 4 characters long. Enter some text: please
object SayPlease extends App {
def report(input: String) = "The input is " + input.length + " characters long."
def inputs = LazyList.continually( readLine("Enter some text: ") )
inputs.takeWhile( _ != "please" ).map(report).foreach(println)
}
readLine
whenever a new element is needed.takeWhile
returns a partial lazy-list that has been cut at
the magic word "please"
.map
generates a lazy-list of reports whose each element is
formed (as needed) by calling readLine
and applying report
to the resulting string. This command also doesn’t prompt the user
for the inputs yet, nor does it call report
on them; it simply
prepares a lazy-list that does that if and when we later access
the elements.foreach
orders the lazy-list to print out the elements of the
report list. Before it can process an elemenrt, the lazy-list
object is forced to determine what that element is by prompting
the user for input and a applying report
. In practice, what
we get is a program that repeatedly receives keyboard input and
reports its length.The convenient factory method LazyList.from
creates an infinite list of numbers:
val positiveNumbers = LazyList.from(1)positiveNumbers: LazyList[Int] = LazyList(<not computed>) positiveNumbers.take(3).foreach(println)1 2 3 LazyList.from(0, 10).take(3).foreach(println)0 10 20 val firstBigSquare = LazyList.from(0).map( n => n * n ).dropWhile( _ <= 1234567 ).headfirstBigSquare: Int = 1236544
More ways to create a LazyList
The iterate
method creates a lazy-list that generates each element
by re-applying a function to the previous element:
def alternating = LazyList.iterate(1)( x => -2 * x )alternating: LazyList[Int] alternating.take(4).foreach(println)1 -2 4 -8
You can use a recursive definition to define any kind of
lazy-list. This simple example does the same as LazyList.from(1)
:
def positiveNumbers(first: Int): LazyList[Int] = first #:: positiveNumbers(first + 1)positiveNumbers: (first: Int)LazyList[Int]
positiveNumbers(1).take(3).foreach(println)1
2
3
#::
combines a single value and a lazy-list,
yielding another lazy-list. The value to the left of the
operator becomes the first element; it’s followed by the
elements of the lazy-list on the right-hand side.Passing unevaluated parameters “by name”
Lazy-lists are based on the idea that a method may receive a parameter that holds an unevaluated expression rather than the value of that expression. Such an unevaluated parameter — a by-name parameter — is evaluated only when (or if) the method reaches a point that actually uses that parameter.
Below is a small example of a by-name parameter.
def printAndReturn(number: Int) = { println("I'll return my parameter " + number) number }printAndReturn: (number: Int)Int def test(number: Int, numberGeneratingExpr: =>Int) = if (number >= 0) numberGeneratingExpr else -1test: (number: Int, numberGeneratingExpr: => Int)Int
=>
. This parameter
is evaluated only when or if it’s used during an invocation
of test
.The output demonstrates how the parameter works:
test(printAndReturn(10), printAndReturn(100))I'll return my parameter 10 I'll return my parameter 100 res185: Int = 100 test(printAndReturn(-10), printAndReturn(100))I'll return my parameter -10 res186: Int = -1
test
.printAndReturn
a second time.Repeating Commands in a Loop
for
loops
A for
loops repeats an operation on each element in a collection (Chapter 5.5):
val myBuffer = Buffer(100, 20, 5, 50)myBuffer: Buffer[Int] = Buffer(100, 20, 5, 50) for (elem <- myBuffer) { println("Current element: " + elem) println("That plus one: " + (elem + 1)) }Current element: 100 That plus one: 101 Current element: 20 That plus one: 21 Current element: 5 That plus one: 6 Current element: 50 That plus one: 51
<-
is followed by an expression that
determines the source of the elements.You’re free to use a combination of instructions in the loop body. You can put in an if
,
for example:
for (currentElem <- myBuffer) { if (currentElem > 10) { println("This element is greater than ten: " + currentElem) } else { println("Here we have a small number.") } }This element is greater than ten: 100 This element is greater than ten: 20 Here we have a small number. This element is greater than ten: 50
A for
loop can iterate over other kinds of collections, too (Chapter 5.6). Here are
some of examples, two with a Range
and one with a String
:
for (number <- 10 to 15) { println(number) }10 11 12 13 14 15 for (index <- myBuffer.indices) { println("Index " + index + " stores the number " + myBuffer(index)) }The index 0 stores the number 100 The index 1 stores the number 20 The index 2 stores the number 5 The index 3 stores the number 50 for (letter <- "test") { println(letter) }t e s t
Here’s one more loop. It iterates over a collection of pairs (see Pairs and Other Tuples
above) that have been generated by zipWithIndex
(see Common Methods on Collections,
above):
for ((element, index) <- myBuffer.zipWithIndex) { println("Index " + index + " stores the number " + element) }Index 0 stores the number 100 Index 1 stores the number 20 Index 2 stores the number 5 Index 3 stores the number 50
You’ll find many more examples of for
loops in Chapters 5.5 and 5.6.
More features of Scala’s for
expressions
Scala’s for
expression is capable of various things that aren’t much
discussed, or needed, in O1. You can, for instance, use a for
to
generate a new collection rather than performing effectful operations.
That requires the additional keyword yield
:
val myVector = Vector(100, 0, 20, 5, 0, 50)myVector: Vector[Int] = Vector(100, 0, 20, 5, 0, 50) for (number <- myVector) yield number + 100res187: Vector[Int] = Vector(200, 100, 120, 105, 100, 150) for (word <- Vector("llama", "alpaca", "vicuña")) yield word.lengthres188: Vector[Int] = Vector(5, 6, 6)
You can also add a filter:
for (number <- myVector; if number != 0) yield 100 / numberres189: Vector[Int] = Vector(1, 5, 20, 2)
In Scala, for
loops are just a different notation for writing higher-order
method calls that invoke foreach
, map
, flatMap
, and filter
; cf.
Processing Collections with Higher-Order Methods, above.
Nested loops
A loop body can contain another loop. This means that the entire inner loop will run every time the body of the outer loop is executed (Chapter 5.6).
Here’s one example:
val numbers = Vector(5, 3)numbers: Vector[Int] = Vector(5, 3) val letters = "abcd"letters: String = abcd for (number <- numbers) { println("Cycle of outer loop begins.") for (letter <- letters) { println(s"the number is $number and the letter is $letter") } println("Cycle of outer loop is over.") }Cycle of outer loop begins. the number is 5 and the letter is a the number is 5 and the letter is b the number is 5 and the letter is c the number is 5 and the letter is d Cycle of outer loop is over. Cycle of outer loop begins. the number is 3 and the letter is a the number is 3 and the letter is b the number is 3 and the letter is c the number is 3 and the letter is d Cycle of outer loop is over.
Nesting and for
You can combine nested traversals in a single for
. The two programs below
do the same thing.
for (number <- numbers) {
for (letter <- letters) {
println(number + "," + letter)
}
}
for (number <- numbers; letter <- letters) {
println(number + "," + letter)
}
do
loops
A do
loop ends in a condition that defines how long the computer will keep repeating
the contents of the loop. Here’s an example from Chapter 8.3:
var number = 1number: Int = 1 do { println(number) number += 4 println(number) } while (number < 10)1 5 5 9 9 13
do
loop has the do
keyword at the top and the while
keyword
at the bottom. The loop body goes in curly brackets between the two.
Don’t omit the brackets.while
comes the looping condition, a Boolean
expression.
This expression is evaluated once every time the loop body has been
executed. If it evaluates to false
, the loop terminates, otherwise
it starts another cycle at the top. The round brackets around the
condition are mandatory.do
loop repeats one or more times. In this example,
there are three iterations. The first ends with number
storing 5,
the second with 9, and the third with 13, at which point the
looping condition is not met.while
loops
A while
loop is very similar to a do
. The difference is that its program code
starts with the looping condition and that condition is checked at the start of
each loop cycle, rather than the end:
var number = 1number: Int = 1 while (number < 10) { println(number) number += 4 println(number) }1 5 5 9 9 13
while
starts the loop. It’s followed by the looping
condition in round brackets. This condition is checked for the
first time before the loop body has been executed even once.number
equals 1, the looping condition number < 10
is true
at the start of the loop. Since that is the case,
this while
produces the same output as the do
loop above.In constrast to the body of a do
loop, which runs one or more times, the body of a
while
loop may not run even once:
var number = 20number: Int = 20 while (number < 10) { println(number) number += 4 println(number) }
For more examples, see Chapter 8.3.
Map
s
A map is a collection whose elements are key–value-pairs (Chapter 8.4). It doesn’t rely on numerical indices; instead, it uses keys for looking up the corresponding values. Key–value pairs are represented as orginary tuples (see Pairs and Other Tuples). The same value may appear multiple times in a map, but the keys must be unique.
Here’s one way to create a Map
:
val finnishToEnglish = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")finnishToEnglish: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
Map
are key–value pairs.Map
has two type parameters: the type of the keys and the type
of the values. In this example, we have a Map
whose keys and
values are both strings.Accessing values: get
, contains
, apply
The contains
method tells us if a given key is present in the map:
finnishToEnglish.contains("tapiiri")res190: Boolean = true finnishToEnglish.contains("Mikki-Hiiri")res191: Boolean = false
You can use get
to fetch the value that matches a given key. The result comes as
an Option
:
finnishToEnglish.get("kissa")res192: Option[String] = Some(cat) finnishToEnglish.get("Mikki-Hiiri")res193: Option[String] = None
You can also access a value as shown below, but then you’ll cause a runtime error if there is no matching key in the map:
finnishToEnglish("kissa")res194: String = cat finnishToEnglish("Mikki-Hiiri")java.util.NoSuchElementException: key not found: Mikki-Hiiri ...
Modifying a Map
Scala’s standard API comes with two different Map
classes, one for mutable maps and one
for immutable ones; immutable Map
s are always available without an import
. The examples
on this page use immutable Map
s unless otherwise specified, but here are a few examples
of effects on a mutable Map
.
import scala.collection.mutable.Mapimport scala.collection.mutable.Map val finnishToEnglish = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")finnishToEnglish: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
Here are two different ways to add a key–value pair to a mutable map (Chapter 8.4):
finnishToEnglish("hiiri") = "mouse"finnishToEnglish += "sika" -> "pig"res195: Map[String, String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, sika -> pig, hiiri -> mouse, laama -> llama)
The same commands work for replacing an existing key-value pair: if the key already exists, the new pair will replace the old one.
Here are two different ways to remove a pair from a mutable map:
finnishToEnglish.remove("tapiiri")res196: Option[String] = Some(tapir) finnishToEnglish -= "laama"res197: Map[String, String] = Map(koira -> puppy, kissa -> cat, sika -> pig, hiiri -> mouse)
Missed lookups and default values: getOrElse
, withDefault
, etc.
When you call getOrElse
, you pass in an expression that specifies a “default value”
(Chapter 8.4):
val finnishToEnglish = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")finnishToEnglish: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama) finnishToEnglish.getOrElse("kissa", "unknown word")res198: String = cat finnishToEnglish.getOrElse("Mikki-Hiiri", "unknown word")res199: String = unknown word
getOrElse
is String
, whereas for get
it
was Option[String]
.If the Map
is mutable, you can also use getOrElseUpdate
. When this method fails to
find the given key, it adds the given value to the Map
, which means that the lookup will
always succeed in the end:
import scala.collection.mutable.Mapimport scala.collection.mutable.Map val finnishToEnglish = Map("kissa" -> "cat", "laama" -> "llama", "tapiiri" -> "tapir", "koira" -> "puppy")finnishToEnglish: Map[String,String] = Map(koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama) finnishToEnglish.getOrElseUpdate("lude", "bug")res200: String = bug finnishToEnglishres201: Map[String,String] = Map(lude -> bug, koira -> puppy, tapiiri -> tapir, kissa -> cat, laama -> llama)
As an alternative to the above methods, you can give the entire Map
a generic default
value (Chapter 8.4):
val finToEng = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog").withDefaultValue("not found")finToEng: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat) finToEng("kissa")res202: String = cat finToEng("Mikki-Hiiri")res203: String = not found
withDefaultValue
to let the Map
know
what it should default to on a failed lookup.withDefault
The previous example used a fixed default value. If you want to customize the
default values, you can use withDefault
to set a “default function” instead:
def report(missingKey: String) = "you looked up " + missingKey + " but to no avail"report: (missingKey: String)String val finToEng = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog").withDefault(report)finToEng: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat) finToEng("kissa")res204: String = cat finToEng("Mikki-Hiiri")res205: String = you looked up Mikki-Hiiri but to no avail
Making a Map
from an existing collection: toMap
, groupBy
You can call toMap
to form a Map
from any collection of pairs (Chapter 9.2):
val animals = Vector("dog", "cat", "platypus", "otter", "llama", "pig")animals: Vector[String] = Vector(dog, cat, platypus, otter, llama, pig) val animalCounts = Vector(2, 12, 35, 5, 7, 5)animalCounts: Vector[Int] = Vector(2, 12, 35, 5, 7, 5) val vectorOfPairs = animals.zip(animalCounts)vectorOfPairs: Vector[(String, Int)] = Vector((dog,2), (cat,12), (platypus,35), (otter,5), (llama,7), (pig,5)) val animalMap = vectorOfPairs.toMapanimalMap: Map[String,Int] = Map(dog -> 2, otter -> 5, platypus -> 35, llama -> 7, cat -> 12, pig -> 5) animalMap("llama")res206: Int = 7
zip
them together to produce a vector that contains pairs.toMap
takes such a collection of pairs and generates a Map
.The groupBy
method constructs a Map
that contains the elements of an existing
collection grouped on the basis of what a given function returns on each of those
elements:
val animalCounts = Vector(2, 12, 35, 5, 7, 5)animalCounts: Vector[Int] = Vector(2, 12, 35, 5, 7, 5) val groupedByParity = animalCounts.groupBy( _ % 2 == 0 )groupedByParity: Map[Boolean,Vector[Int]] = Map(false -> Vector(35, 5, 7, 5), true -> Vector(2, 12)) val animals = Vector("dog", "cat", "platypus", "otter", "llama", "pig")animals: Vector[String] = Vector(dog, cat, platypus, otter, llama, pig) val groupedByLength = animals.groupBy( _.length )groupedByLength: Map[Int,Vector[String]] = Map(8 -> Vector(platypus), 5 -> Vector(otter, llama), 3 -> Vector(dog, cat, pig))
Both toMap
and groupBy
return immutable maps.
For more examples, see Chapter 9.2.
Other methods on Maps
: keys
, values
, map
, etc.
Map
s are collections and, as such, provide many of the same methods as other
collections do (see Collection Basics, Common Methods on Collections, and Processing
Collections with Higher-Order Methods). They don’t have methods that rely on
numerical indices, but methods such as isEmpty
, size
, and foreach
work fine, as do
many others:
val finToEng = Map("kissa" -> "cat", "tapiiri" -> "tapir", "koira" -> "dog")finToEng: Map[String,String] = Map(koira -> dog, tapiiri -> tapir, kissa -> cat) finToEng.isEmptyres207: Boolean = false finToEng.sizeres208: Int = 3 finToEng.foreach(println)(koira,dog) (tapiiri,tapir) (kissa,cat)
The keys
and values
methods (Chapter 8.4) are specific to Map
s. They return
collections that contain just the keys or just the values of the Map
:
finToEng.keys.foreach(println)koira tapiiri kissa finToEng.values.foreach(println)dog tapir cat
The map
method (Chapter 8.4) of a Map
operates on key–value pairs:
finToEng.map( pair => pair._1 -> pair._2.length )res209: Map[String,Int] = Map(kissa -> 3, tapiiri -> 5, koira -> 3)
As shown above, the method generates a new Map
in which the original key–value pairs
have been replaced by the given function’s return values.
For the full list of methods, go to the official documentation.
Supertypes and Subtypes
To represent a supertype and its subtypes, you can either define a trait (Chapter 7.2) and have the subtypes extend it or define a superclass (Chapter 7.3) and inherit from it.
Traits
You define a trait much like you define a class. This trait from Chapter 7.2 represents the abstract concept of a shape:
trait Shape {
def isBiggerThan(another: Shape) = this.area > another.area
def area: Double
}
isBiggerThan
method that compares the areas
of two shapes.area
method for computing the size of
the shape. This method is abstract: it has no body and you can’t
invoke it as such. We’ll define the algorithms for computing areas
differently for the different subtypes that extend Shape
(see
below).isBiggerThan
takes a reference to any Shape
object. All such
objects will have some kind of implementation for area
, so
we can call that method on the parameter.Extending a trait
The two classes below mix in the Shape
trait (Chapter 7.2). They define
subtypes of the more general Shape
supertype:
class Circle(val radius: Double) extends Shape {
def area = scala.math.Pi * this.radius * this.radius
}
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {
def area = this.sideLength * this.anotherSideLength
}
extends
marks Circle
as a subtype of Shape
.
This implies that all objects of type Circle
are not only
circles but shapes, too. They have, for instance, the
isBiggerThan
method defined in the Shape
trait.A class can extend multiple traits. The extends
keyword should appear only once though;
use with
for the other supertypes:
class X extends A with B with C with D with Etc
Static and dynamic types
Chapter 7.2 draws a distinction between static types and dynamic types:
var myShape: Shape = new Circle(1)myShape: o1.shapes.Shape = o1.shapes.Circle@1a1a02e myShape = new Rectangle(10, 5)myShape: o1.shapes.Shape = o1.shapes.Rectangle@7b519d
myShape
has the static type Shape
. It may refer
to any object of type Shape
, which might be a Circle
or a
Rectangle
or an instance of some other subtype of Shape
.
The static type of a variable or expression can always be
determined from the program code.myShape
is first assigned a value
whose dynamic type is Circle
. That value is then replaced with
another whose dynamic type is Rectangle
. The value’s dynamic
type is required to be compatible with the variable’s static type.All Scala objects have the isInstanceOf
method, which examines the dynamic type of an
object. The code below determines that myShape
currently stores a reference to an object
that is both a Rectangle
and a Shape
:
myShape.isInstanceOf[Rectangle]res210: Boolean = true myShape.isInstanceOf[Shape]res211: Boolean = true
In the example above, we had explicitly set the static type of myShape
as Shape
.
Below, we don’t, which is why the attempted assignment fails:
var experiment = new Circle(1)experiment: o1.shapes.Circle = o1.shapes.Circle@1c4207e
experiment = new Rectangle(10, 5)<console>:11: error: type mismatch;
found : o1.shapes.Rectangle
required: o1.shapes.Circle
experiment = new Rectangle(10, 5)
^
Circle
, so the
variable can only store references to Circle
objects, no
other shapes.Static types restrict what we can do with a value (Chapter 7.2):
var test: Shape = new Circle(10)test: o1.shapes.Shape = o1.shapes.Circle@9c8b50
test.radius<console>:12: error: value radius is not a member of o1.shapes.Shape
test.radius
^
test
is Shape
. An arbitrary Shape
object
doesn’t have a radius
even though circles do.You can use match
to make a decision based on a value’s dynamic type:
test match { case actualCircle: Circle => println("It's a circle and its radius is " + 1«actualCircle.radius») case _ => println("It's not a circle.") }It's a circle and its radius is 10.0
Inheritance
A class can inherit another class. In this example from Chapter 7.3, we have a class
Square
that inherits class Rectangle
:
class Rectangle(val sideLength: Double, val anotherSideLength: Double) extends Shape {
def area = this.sideLength * this.anotherSideLength
}
class Square(size: Double) extends Rectangle(size, size)
extends
with the superclass’s name: the subclass
Square
inherits from Rectangle
. This gives all Square
objects
the additional type of Rectangle
(and Shape
, since Rectangle
extends the Shape
trait).Square
takes a constructor parameter that determines the length
of each side.Square
object is created, we initialize
a Rectangle
so that each of its two constructor parameters (each
side length) gets the value of the new Square
’s single constructor
parameter.In a concrete class, all methods have an implementation. You can also define an abstract class that, like a trait, may have abstract methods. Here’s an example from Chapter 7.3:
abstract class Transaction(val vatAdded: Boolean) {
def totalPrice: Double
def priceWithoutTax = if (this.vatAdded) this.totalPrice / 1.24 else this.totalPrice
}
abstract
keyword turns the class into an abstract class.
This class can’t be directly instantiated.totalPrice
method is abstract. Any concrete subclasses need
to have an implementation for this method so that all Transaction
objects can actually run this method.This table from Chapter 7.3 juxtaposes traits, abstract classes, and ordinary superclasses:
Trait | Abstract superclass | Concrete superclass | |
---|---|---|---|
Can it have abstract methods? | Yes. | Yes. | No. |
Can it be directly instantiated using new ? |
No. | No. | Yes. |
Can it take constructor parameters? | No. | Yes. | Yes. |
Can a class extend several of them
(using
extends and with )? |
Yes. | No. | No. |
You can combine the techniques. For instance, you can have a class inherit from a superclass and extend a number of traits. Or you can have a class extend a trait.
Singleton objects can also extend classes and traits.
Reimplementing a method with override
A subtype can override
a supertype method (Chapters 2.4 and 7.3). The toString
method
is overridden particularly often, but here’s a different example:
class Super {
def one() = {
println("superclass: one")
}
def two() = {
println("superclass: two")
}
}
class Sub extends Super {
override def one() = {
println("subclass: one")
}
override def two() = {
println("subclass: two")
super.two()
}
}
val experiment = new Subexperiment: Sub = Sub@1bd9da5 experiment.one()subclass: one experiment.two()subclass: two superclass: two
one
method of a Sub
object works independently of
anything that the superclass does: this implementation
replaces the one in the superclass.two
method, we’ve chosen to
call the superclass’s version of the same method, so...Sub
object first generates the output specified in
the subclsses, then does whatever the superclass method
does.The super
keyword (note the lower case) refers to the supertype’s definition. It’s
available not just in overridden methods but throughout the body of any subtype.
Scala’s class hierarchy
All Scala objects are of type Any
. Any
has two direct subclasses, AnyVal
and AnyRef
:
AnyVal
is a superclass of the common data typesInt
,Double
,Boolean
,Char
,Unit
, and a few others. It’s relatively uncommon to extendAnyVal
in an application.AnyRef
, also known asObject
, is a superclass for all the classes and singleton objects that don’t derive fromAnyVal
, such asString
andVector
. Any classes that you define automatically inheritAnyRef
unless you specify otherwise.
For a further discussion, see Chapter 7.3.
Constraints on subtyping: sealed
and final
The word sealed
at the top of a trait or class means that you are allowed to directly
extend that trait or class only within the same file (Chapter 7.2). For instance, the
API class Option
is defined like this:
sealed abstract class Option /* Etc. */
Nothing can extend Option
except the singleton None
and the subclass Some
, which
are defined in the same file. This guarantees that every last Option
object is either
None
or a Some
.
The word final
(Chapter 7.3) similarly prevents extending a class altogether. You
can also write final
in a method definition (before def
): this prevents subtypes
from overriding the method.
Random Numbers
The singleton object Random
has methods that generate (pseudo)random numbers:
import scala.util.Randomimport scala.util.Random Random.nextInt(10)res212: Int = 8 Random.nextInt(10)res213: Int = 6 Random.nextInt(10)res214: Int = 2
The singleton Random
accesses the computer’s clock to obtain a random seed for its
algorithm. Alternatively, you can create a Random
object that uses a custom seed:
val myGenerator1 = new Random(74534161)myGenerator1: Random = scala.util.Random@75fbc2df val myGenerator2 = new Random(74534161)myGenerator2: Random = scala.util.Random@3f92984e
myGenerator1.nextInt(100)res215: Int = 53 myGenerator1.nextInt(100)res216: Int = 38 myGenerator1.nextInt(100)res217: Int = 97 myGenerator2.nextInt(100)res218: Int = 53 myGenerator2.nextInt(100)res219: Int = 38 myGenerator2.nextInt(100)res220: Int = 97
nextInt
the same way on both generators,
we get two identical sequences of “random” numbers.Random
objects have other randomizing methods beyond nextInt
. One worth mentioning
here is shuffle
(Chapter 7.4):
val numbers = (1 to 10).toVectornumbers: Vector[Int] = Vector(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) Random.shuffle(numbers)res221: Vector[Int] = Vector(8, 9, 7, 4, 6, 1, 10, 2, 5, 3) Random.shuffle(numbers)res222: Vector[Int] = Vector(8, 6, 4, 5, 9, 1, 3, 7, 2, 10)
For more on randomness, see Chapter 3.6.
Working with Files
The example program below reads in a text file (Chapter 12.1). It prints out the lines from
example.txt
, prefixing each one with a line number:
import scala.io.Source
object NumberedPrintout extends App {
val file = Source.fromFile("subfolder/example.txt")
try {
var lineNumber = 1
for (line <- file.getLines) {
println(lineNumber + ": " + line)
lineNumber += 1
}
} finally {
file.close()
}
}
fromFile
takes in a file path and returns an object of type
Source
that is capable of accessing the file. The path may
be relative (as shown here) or absolute.getLines
method. (There are alternative ways to
iterate over the contents of a file; see Chapter 12.1.)try
–finally
construct ensures that the file-closing
code in the finally
block will be executed even if the attempt
to read the data from the file fails for some reason.The example program below writes text into a file:
import java.io.PrintWriter
import scala.util.Random
object WritingExample extends App {
val fileName = "examplefolder/random.txt"
val file = new PrintWriter(fileName)
try {
for (n <- 1 to 10000) {
file.println(Random.nextInt(100))
}
println("Created a file " + fileName + " that contains pseudorandom numbers.")
println("In case the file already existed, its old contents were replaced with new numbers.")
} finally {
file.close()
}
}
PrintWriter
object as shown. Pass in the
name of the file you intend to create or rewrite.println
method writes a single line of text into the file.Graphical User Interfaces
Programmers use different libraries for writing graphical user interfaces. O1’s ebook features two libraries: the GUI tools in O1Library and Scala’s more generic GUI library, Swing.
O1Library’s GUI tools
The key component of O1’s GUI toolkit is the class o1.View
. Below is an example that summarizes
some of its main features.
The basic idea is this: a View
is a window that graphically displays an object that serves as
an application’s domain model (Chapter 2.7). In the example below, the model is an instance
of this toy class:
// Each "Thing" is a mutable object. It has a location and a color.
class Thing(var color: Color) {
var location = new Pos(10, 10)
def move() = {
this.location = this.location.add(1, 1)
}
def returnToStart() = {
this.location = new Pos(10, 10)
}
}
Let’s write a GUI that looks like this and displays a Thing
as a circle against a two-color
background:
Here’s the code that implements the GUI:
object ViewExample extends App {
val thing = new Thing(Blue)
val background = rectangle(200, 400, Red).leftOf(rectangle(200, 400, Blue))
val gui = new View(thing, 10, "A Diagonally Moving Thing") {
def makePic = {
val picOfThing = circle(20, thing.color)
background.place(picOfThing, thing.location)
}
override def onTick() = {
thing.move()
}
override def onMouseMove(mousePos: Pos) = {
thing.color = if (mousePos.x < 200) Red else Blue
}
override def onClick(click: MouseClicked) = {
if (click.clicks > 1) {
thing.returnToStart()
}
}
override def isDone = thing.pos.x > 400
}
gui.start()
}
View
, you need to specify which object it is
a view to (here: a Thing
object). You can use optional parameters
to set the tick rate of the app’s clock (here: 10) and the title
of the GUI window.View
object needs a makePic
method that determines which
image to display onscreen. Here, we form the image by placing a
small circle against a rectangular background image.MouseClicked
object that describes the event
and ask it to provide the number of consecutive clicks (Chapter 3.6).isDone
method defines when the GUI should stop responding
to events. In this app, that happens if the “thing” reaches a
location far enough on the right.View
object isn’t enough to display the window and
start the clock. You do that by calling start
.For more information, see Chapters 3.1, 3.6, and the Scaladocs.
The Swing GUI library
Chapter 12.3 is an introduction to the GUI library Swing. This example from that chapter demonstrates several of the library’s key features:
import scala.swing._
import scala.swing.event._
object EventTestApp extends SimpleSwingApplication {
val firstButton = new Button("Press me, please")
val secondButton = new Button("No, press ME!")
val prompt = new Label("Press one of the buttons.")
val allPartsTogether = new BoxPanel(Orientation.Vertical)
allPartsTogether.contents += prompt
allPartsTogether.contents += firstButton
allPartsTogether.contents += secondButton
val buttonWindow = new MainFrame
buttonWindow.contents = allPartsTogether
buttonWindow.title = "Swing Test App"
this.listenTo(firstButton)
this.listenTo(secondButton)
this.reactions += {
case clickEvent: ButtonClicked =>
val clickedButton = clickEvent.source
val textOnButton = clickedButton.text
Dialog.showMessage(allPartsTogether, "You pressed the button that says: " + textOnButton, "Info")
}
def top = this.buttonWindow
}
clickEvent
, which stores a ButtonClicked
object that
represents the GUI event that occurred.SimpleSwingApplication
needs a top
window that gets
displayed as soon as the application is launched.Reserved Words
The following are reserved words in Scala and therefore cannot be used as identifiers:
abstract case catch class def do else extends false final
finally for forSome if implicit import lazy match new null
object override package private protected return sealed super then this
throw trait try true type val var while with yield
_ : = => <<: <% >: # @ ⇒ ←
Feedback
Credits
Thousands of students have given feedback that has contributed to this ebook’s design. Thank you!
The ebook’s chapters, programming assignments, and weekly bulletins have been written in Finnish and translated into English by Juha Sorva.
The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.
The automatic assessment of the assignments has been developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Joonatan Honkamaa, Jaakko Kantojärvi, Niklas Kröger, Teemu Lehtinen, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, and Aleksi Vartiainen.
The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.
The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio did the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.
The other diagrams and interactive presentations in the ebook are by Juha Sorva.
The O1Library software has been developed by Aleksi Lukkarinen and Juha Sorva. Several of its key components are built upon Aleksi’s SMCL library.
The pedagogy of using O1Library for simple graphical programming (such as Pic
) is
inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and
Krishnamurthi and Picturing Programs by Stephen Bloch.
The course platform A+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services. Markku Riekkinen is the current lead developer; dozens of Aalto students and others have also contributed.
The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It was created by Nikolai Denissov, Olli Kiljunen, Nikolas Drosdek, Styliani Tsovou, Jaakko Närhi, and Paweł Stróżański with input from Juha Sorva, Otto Seppälä, Arto Hellas, and others.
For O1’s current teaching staff, please see Chapter 1.1.
Additional credits appear at the ends of some chapters.