Luet oppimateriaalin englanninkielistä versiota. Mainitsit kuitenkin taustakyselyssä osaavasi suomea. Siksi suosittelemme, että käytät suomenkielistä versiota, joka on testatumpi ja hieman laajempi ja muutenkin mukava.
Suomenkielinen materiaali kyllä esittelee englanninkielisetkin termit.
Kieli vaihtuu A+:n sivujen yläreunan painikkeesta. Tai tästä: Vaihda suomeksi.
Chapter 1.8: Functions, Types, and Errors
More on Function Calls and the Call Stack
Take a look at the program code below. It features a custom function greatestDistance
that takes in the x and y coordinates of three points and determines the longest distance
between any two of these points. The function makes use of another custom function,
distance
that, as you saw in the previous chapter, determines the distance between two
given points.
def distance(x1: Double, y1: Double, x2: Double, y2: Double) = hypot(x2 - x1, y2 - y1)
def greatestDistance(x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double) =
val first = distance(x1, y1, x2, y2)
val second = distance(x1, y1, x3, y3)
val third = distance(x2, y2, x3, y3)
max(max(first, second), third)
There are two senses in which a function can be “inside” another:
Also, you may call a function within the implementation of
another function. Here the implementation of greatestDistance
calls distance
(several times).
The following animation elaborates.
I like it that the animations show how the computer “thinks”. I mean, it doesn’t really think but just mechanically follows each function, starting with the inner ones and proceeding outwards. It’s easier to read code when you can think like the computer.
Before Our Next Coding Session, Here’s a Summarizing Exercise on Functions
As we progress towards more complex and delightful programs, it’s necessary for you to learn to draw reliable inferences about how a given piece of code works when it runs. Or about a piece of code that you wrote but that fails to work right.
Study the following program. It doesn’t accomplish anything useful as such, but it serves as an exercise on function calls, return values, and printing.
Programming Exercise: leaguePoints
and teamStats
In this assignment, you’ll create two functions, using one as a building block for the other.
Programming is like building with smart Lego that you design yourself!
—origin unknown
Task description
In the file week1.scala
, write two effect-free functions that can be used to
compute the league points of a sports team, given the team’s results.
The first of the functions should match this specification:
Its name is
leaguePoints
.As parameters, it expects the number of wins and the number of draws that the team has played, in that order, as integers.
It returns — but does not print! — the total league points of the team as an integer. A win is worth three points and a draw is worth one point (and a loss is worth no points at all).
The second function should match this specification:
Its name is
teamStats
.As parameters it expects, in order, the name of the team (a string) and the numbers of wins, draws, and losses (integers).
It returns (but does not print!) a string in the format "Name: X/N wins, Y/N draws, Z/N losses, P points". For instance, given the parameter values "Liverpool FC", 8, 7, and 7, the function returns the string "Liverpool FC: 8/22 wins, 7/22 draws, 7/22 losses, 31 points"
Workflow
Create the function
leaguePoints
. Follow the workflow suggested in Chapter 1.7’s programming assignments. Don’t forget to test that your function works before you continue.In the same manner, write and test the function
teamStats
. Here are a few additional tips:When writing a function body with multiple commands, remember to indent it.
To obtain the total points of the team, call the
leaguePoints
function you created earlier.Your code will be more readable if you use a local variable to store the total number of games played by the team.
Submit your solution only when you have written and tested both functions.
A+ presents the exercise submission form here.
More Exercises
Additional practice: Feet and inches, the other way around
This exercise continues the theme of imperial units from Chapter 1.7. You’ll create three one-liner functions, the first of which will be useful for implementing the other two.
First, write an effect-free function toInches
that, given a length
in meters, returns the equivalent number of inches. An inch is 2.54 cm.
Here are some examples:
toInches(1.8)res4: Double = 70.86614173228347 toInches(0.0254)res5: Double = 1.0
Then create two functions that you can use in combination to convert a given number of meters into whole feet and leftover inches. For instance, 1.8 meters is five feet and about eleven inches. Here’s how the two functions should work:
wholeFeet(1.8)res6: Double = 5.0 remainingInches(1.8)res7: Double = 10.866141732283475
Make use of toInches
as you implement these functions. Revisit
Chapter 1.6 to review the tools available in package scala.math
.
Enter the functions in week1.scala
.
A+ presents the exercise submission form here.
A harder version of see-above
Find out on your own how to use pairs (pari) in Scala. Apply what you learned to solve the above problem differently.
Create toInches
as suggested above. Then implement a function
toFeetAndInches
that combines the functionality of wholeFeet
and remainingInches
. The function should return the numbers
that represent feet and inches as a pair. Like so:
toFeetAndInches(1.8)res8: (Double, Double) = (5.0,10.866141732283467) toFeetAndInches(0.0254)res9: (Double, Double) = (0.0,1.0)
A+ presents the exercise submission form here.
We’ll have more to say about pairs in Chapter 9.2.
Programming Exercise: verbalEvaluation
Let’s get back to course grades and create an effect-free function that produces a textual assessment of a student’s work in our imaginary example course. Here’s a template for the function:
def verbalEvaluation(projectGrade: Int, examBonus: Int, participationBonus: Int)
val descriptions = Buffer("failed", "acceptable", "satisfactory", "good", "very good", "excellent")
// PLEASE FILL OUT THIS PART OF THE SOLUTION. YOU CAN REMOVE THIS COMMENT.
The function should work as follows:
The function’s three parameters represent a project grade and bonuses for an exam and participation, exactly as in
overallGrade
from Chapter 1.7.Instead of an integer, this function should return a string description that matches the student’s overall grade. For instance, a grade of two is described as "satisfactory" and a grade of five as "excellent".
Implement the function properly:
Copy the above code template into
week1.scala
.There is a small but serious error in the given template code! You’ll need to fix it.
IntelliJ’s description of the error isn’t too fantastic, for a reason that we’ll discuss at the end of this chapter. But the fix you need to apply is very simple.
Fill out the function body. Use the buffer that’s already been defined for you as well as the
overallGrade
function from Chapter 1.7. If you combine these two, the solution is straightforward.
A+ presents the exercise submission form here.
Programming Exercise: doubleScore
The next function is effectful: it modifies the contents of a given buffer (so in
this respect it is similar to Chapter 1.6’s removeNegatives
).
Task description
Let’s consider an imaginary game where multiple players compete with each other and
collect points. As the game progresses, a player’s score may occasionally double, but
players may also suffer losses. In this assignment, you’ll create in week1.scala
an
effectful function that doubles a given player’s score. The function should work as
follows:
Its name is
doubleScore
.As its first parameter, it receives a reference to a
Buffer
whose elements are integers that represent the current scores of each player.As its second parameter, it receives an integer that determines which player’s score should be doubled: one means the first player’s score, two the second player’s, and so forth.
It modifies the contents of the given buffer so that the targeted player’s score becomes twice whatever it was before.
Here’s an example scenario:
val scoresOfEachPlayer = Buffer(2, 10, 5, 2)scoresOfEachPlayer: Buffer[Int] = ArrayBuffer(2, 10, 5, 2) doubleScore(scoresOfEachPlayer, 3)doubleScore(scoresOfEachPlayer, 4)scoresOfEachPlayerres10: Buffer[Int] = ArrayBuffer(2, 10, 10, 4)
Instructions and hints
You’ll need to indicate that the first parameter is a buffer whose elements are integers. Use square brackets around the buffer’s type parameter as in Chapter 1.5.
The second parameter identifies the target player. It uses a one-based numbering scheme whereas a buffer’s indices run from zero upwards (Chapter 1.5). You’ll need to take this into account.
Don’t worry about special cases such as what happens if someone passes in a player number that’s too large or too small. O1’s programming assignments will specifically tell you when you’re expected to take care of invalid inputs.
You may assume that each player’s score is a positive integer.
The function doesn’t need to return anything.
A single assignment command that targets the right index in the buffer will do for a function body.
A+ presents the exercise submission form here.
Programming Exercise: penalize
Let’s remain with the same imaginary game. Now you’ll create an effectful function that reduces a player’s score. It should work like this:
Its name is
penalize
.As in the previous function, the first parameter refers to a buffer that contains the players’ scores and the second parameter is the number of the target player.
The third parameter indicates the size of the penalty: how many points are to be subtracted from the target player’s score. You can assume that this parameter is a positive number.
However, the rules of the game dictate that a player’s score can never be reduced to zero or below; a player will always have at least a single point. If a player receives a penalty that would violate this rule, the player’s score will only be reduced down to one.
The function returns an integer that indicates how many points were actually removed from the target player.
Here’s a usage scenario:
val scoresOfEachPlayer = Buffer(2, 10, 5, 2)scoresOfEachPlayer: Buffer[Int] = ArrayBuffer(2, 10, 5, 2) penalize(scoresOfEachPlayer, 2, 3)res11: Int = 3
We subtract three from the score of player number two. The return value indicates that three points were successfully removed.
A look at the scores buffer confirms that the points reduction has happened. The second score has dropped to seven:
scoresOfEachPlayerres12: Buffer[Int] = ArrayBuffer(2, 7, 5, 2)
Let’s give the same player a twelve-point penalty:
penalize(scoresOfEachPlayer, 2, 12)res13: Int = 6 scoresOfEachPlayerres14: Buffer[Int] = ArrayBuffer(2, 1, 5, 2)
However, only six points were successfully removed...
... because a player must always have at least one point.
If it seems clear to you how to solve this assignment, by all means go ahead and write the entire solution right now. Otherwise, we recommend approaching the solution in two stages:
Stage 1 of 2: don’t worry about the return value
Write a version that only reduces the target player’s score in the buffer. Don’t concern yourself, yet, with whether the function returns the right value.
You can use Scala’s library function min
(Chapter 1.6) to determine how many points
can be removed. Another approach makes use of max
. There are other ways to make the
function work, too, and you’re free to use any of them.
Test your function to ensure that it works!
Stage 2 of 2: sort out the return value
You already know that in Scala, what a function returns is determined by the last command that is executed as part of the function. A beginner’s first attempt at sketching out an algorithm for this function might thus look something like this:
Reduce the player’s score, but not below one.
Calculate the number of points that were removed and return that result.
The problem is that in order to compute the return value, we need both the third parameter (the size of the penalty) and the player’s original score. Once we’ve already applied the reduction, the original score is no longer stored anywhere and there is no way we can compute the return value at Step 2.
Another attempt:
Calculate the number of points that can be removed and store that result.
Reduce the player’s score by that amount.
Return the amount stored at Step 1.
This version is better, because it computes the future return value before actually applying the penalty.
In order to implement this improved algorithm, you must store the actual size of the penalty at Step 1. That’s well within your grasp: use a local variable.
Hints
Bear in mind that a multi-line function returns the value of the expression that was evaluated last. Such an expression can be simply the name of a variable.
If you want, you can view the following animation. It presents one way to solve this assignment. (The animation does not show the program code; that’s something you’ll have to write yourself.)
There is something worth mentioning in the animation apart from the solution itself.
It illustrates that penalize
receives a reference to a buffer, not a copy the buffer
with identical but separate contents. This is why we can observe a change in the buffer
through another reference that points to the same buffer.
A+ presents the exercise submission form here.
Functions with No Parameters
All the functions that we’ve discussed so far have taken at least one parameter. It’s also possible to define a function that takes no parameters.
def onePlusOne = 1 + 1
No round brackets, no parameter list.
That function always returns the same number:
onePlusOneres15: Int = 2 onePlusOneres16: Int = 2
Different Kinds of Errors
Ninety percent of your time will be spent searching for errors in the ten percent of code that you last wrote.
—origin unknown
Locating errors is an essential activity in programming and takes up a large chunk of programmers’ time. Now that you have your hands dirty writing program code, it’s good to recognize the main types of errors.
Errors at compile time
Compilation-time errors (käännösaikainen virhe) can be detected automatically even before the program is run. The name refers to how these errors can be spotted by auxiliary programs called compilers, which convert program code into a form that is more readily executable by a computer. (More on that in Chapter 5.4.)
Compile-time error messages result from mistakes such as incorrect punctuation and (in Scala) attempting to assign a value of an incompatible type to a variable. Many compile-time errors are syntax errors (syntaksivirhe): they indicate violations of the programming language’s syntactical rules (grammar).
For the experienced programmer, most compilation-time errors are effortless to fix. For the beginner, too, this is typically the least problematic category of errors.
In Chapter 1.7, we discussed how IntelliJ highlights some errors in the editor and displays error messages in the Build tab. All of those are examples of compilation-time errors (even though IntelliJ red-alerts many of them instantly in the editor already before full compilation).
Errors at runtime
Runtime errors (ajonaikainen virhe) are more irksome: they make themselves known only while the program is being executed and may not show up for all input values.
The classic example of a runtime error is division by zero: if it transpires that we’ve used an expression where the denominator evaluates to zero, a runtime error occurs and will “crash” the program (i.e., abruptly abort the program’s execution) unless we’ve specifically prepared for that contingency.
Indexing errors are another example. You saw examples of those in Chapter 1.5 when you attempted to use excessively large and small integers to access a buffer’s elements.
Exception (poikkeus) is an alternative name for some runtime errors.
We’ll return to runtime errors and program crashes in Chapter 4.2.
Note that in the REPL, the distinction between compile-time and runtime errors blurs because our code is (first) compiled and (then) run as soon as we type it in.
Logical errors
A logical error (looginen virhe) is what we call it when our program “works” in technical terms but does something other than what we intended, possibly something entirely pointless. Choosing the wrong arithmetic operation is an example of a logical error.
Some logical errors are easy to spot by looking at the code or exploring program behavior. Others are harder. In any case, locating logical errors is up to the programmer, since the computer is incapable of alerting us to them.
On Data Types and Scala
Let’s round off Week 1 with a few observations on how data types are used in Scala programs and how this has already manifested itself in the code that we’ve written. The following gray-bordered box sets up the topic but isn’t strictly necessary for our purposes in O1. Feel free to skip it if you’re in a hurry. The text that follows the box is more crucial.
On Type Systems
The nature of a programming language is greatly influenced by its type system (tyyppijärjestelmä): the general rules that govern the data types of program components and the way those types impact on how people use the language. The details or theory of type systems aren’t part of O1, but we may take an educational dip into some basic concepts.
Type safety
Scala is a very type safe (tyyppiturvallinen) language. Each value in a Scala program has a specific data type, and that type serves as a constraint on the operations that we can apply to the value. Integers can be used in arithmetics, strings can be concatenated, and buffers can receive new elements; on the other hand, an integer can’t store elements, and an attempt to do such a thing brings a timely error message.
The classic example of a programming language with an unsafe type system is the C language. In C, the programmer can use instructions that “go against the types”, with results that depend on context and are, in some cases, unpredictable. Type unsafety permits certain alternative approaches for solving problems but increases the likelihood of errors that slow down the programmer (even an accomplished one) and may damage the final product.
Static vs. dynamic typing
In Chapter 1.2, we pointed up that programs have a dual nature as static and dynamic, a fact that is also evident from the animations embedded in this ebook.
We can also observe this duality in type systems.
Scala is statically typed (staattisesti tyypitetty): the parts of a Scala program have types that are well defined already in the program’s static form, the program code. For instance, each Scala variable and expression has a specific data type that can be determined by looking at the code. This means, among other things, that even before we run our program, our tools can warn us about commands that are invalid, such as attempting to pass an integer as a parameter where a string is required. Static typing can also result in more efficient (faster) programs and otherwise contribute towards better programming tools. These other advantages of static typing are most prominent in larger programs that aim for high quality and reliability.
(It may sound like a dumb mistake to use a value with the wrong type as, say, a function parameter. However, such mistakes are far more common and understandable than you might think, even in programs written by professional programmers. You’ll notice this yourself sooner or later.)
In a dynamically typed (dynaamisesti tyypitetty) language, the parts of program code don’t have types as such. For instance, in the Python programming language, variables don’t have types, and you can assign any kinds of values to a variable; exactly what type of value gets assigned to a variable may be determined by events that happen during a particular program run and influenced by user input. This approach is more flexible in some respects and makes some aspects of programming convenient; a dynamic typed language may also be simpler than a statically typed one. The downsides of dynamic typing include an increased risk of programming errors and the related problem that many errors cannot be spotted without running the program on various inputs.
Programmers disagree among themselves, sometimes vehemently, about the relative merits of static and dynamic typing. The topic has been the cause of many a religious war, civilized conversation, and minor dispute.
Type annotations
In many statically typed languages (such as Java), the programmer writes type annotations all over program code. In these languages, when you define a variable, you’ll always or usually also write down the variable’s data type, and the type of most or all return values must also be explicitly stated. On the other hand, in dynamically typed languages such as Python, such type annotations don’t feature at all (usually).
Scala is statically typed, but we haven’t written many type annotations in our programs. It was only recently, in Chapter 1.7, that we first paid proper attention to the matter, as we brought up the need to annotate each parameter variable with a type. (Chapter 1.5 foreshadowed this: we needed to declare an explicit type for the elements of an empty buffer.) We have been able to largely ignore the matter so far, because much of the static type information inherent in a Scala program can be inferred automatically. Consequently, there are many things that we can accomplish in Scala with the convenience usually associated with dynamically typed languages.
Type inference in Scala
There are certain parts of a Scala program where we must write type annotations. The parameter variables of functions are one such part; we follow each parameter’s name with a colon and a data type. It’s not just parameters that have data types in Scala, but the types of other constructs can usually be determined automatically by the type inference (tyyppipäättely) capability that is built into the Scala toolkit.
For example, we have defined variables like this:
val number = 123
val text = "The number is " + number + "."
Those commands are in fact abbreviated versions of these type-annotated ones:
val number: Int = 123
val text: String = "The number is " + number + "."
The shorter variants work, because the types of the variables can be readily inferred from the values that we assign to them. It’s perfectly legal to write the type annotations — as we did right there — but it’s unnecessary and we don’t usually do it.
A familiar function definition appears below. In this piece of code, too, we’ve actually omitted a data type and left it for the computer to determine:
def average(first: Double, second: Double) = (first + second) / 2
The above is short for this:
def average(first: Double, second: Double): Double = (first + second) / 2
The final Double
annotation after the parameter list indicates the type of the return
value: this function takes in two decimal numbers and also returns a decimal number.
However, the return type can be automatically inferred from the expression (first +
second) / 2
, given that the parameters’ types are known. So we can skip writing that
bit if we want.
Later in O1, we’ll come across some other circumstances where it’s necessary to write explicit type annotations in Scala code.
About that earlier error
Remember the error in the given template for verbalEvalution
? This one:
def verbalEvaluation(projectGrade: Int, examBonus: Int, participationBonus: Int)
val descriptions = Buffer("failed", "acceptable", "satisfactory", "good", "very good", "excellent")
// ...
IntelliJ’s editor highlights the end of the def
line at the
top. The error message opines: Missing return type. Yeah, well, it’s
true that you could write a return type there, too,
but the actual problem is the missing equals sign.
The general lesson here is that although the computer can spot certain errors for us, its decriptions of the problem and what to do about it aren’t always apt. Some of the suggestions you’ll see in error messages do hit the mark, but ultimately the responsibility to analyze the situation and apply the right fix rests with the programmer.
Summary of Key Points
A function can call another function.
When that happens, a new frame is created on top of the call stack. The calling function’s execution is suspended as it waits for the other function to finish. Only the top frame of the call stack is actively used.
You can write a function that performs a subtask and then make use of that function in the implementation of another custom function.
Programs can have compilation-time errors, runtime errors, and logical errors.
There are a few places in Scala programs that require type annotations. Parameter variables are the most obvious example. However, Scala tools are capable of inferring the types of many program components automatically.
Links to the glossary: function, function call, call stack, frame, local variable; error; type annotation, type inference.
What Now and What Next?
You can now write your own program components — functions — constructing them from assorted materials such as numbers, buffers, and other functions. But we’re still a bit of ways from creating an entire program with multiple components that work in unison.
There are a number of approaches to building a larger program. We’ll adopt one good approach in Week 2.
On Academic Integrity
Feedback
Please note that this section must be completed individually. Even if you worked on this chapter with a pair, each of you should submit the form separately.
Credits
Thousands of students have given feedback and so contributed to this ebook’s design. Thank you!
The ebook’s chapters, programming assignments, and weekly bulletins have been written in Finnish and translated into English by Juha Sorva.
The appendices (glossary, Scala reference, FAQ, etc.) are by Juha Sorva unless otherwise specified on the page.
The automatic assessment of the assignments has been developed by: (in alphabetical order) Riku Autio, Nikolas Drosdek, Kaisa Ek, Joonatan Honkamaa, Antti Immonen, Jaakko Kantojärvi, Onni Komulainen, Niklas Kröger, Kalle Laitinen, Teemu Lehtinen, Mikael Lenander, Ilona Ma, Jaakko Nakaza, Strasdosky Otewa, Timi Seppälä, Teemu Sirkiä, Joel Toppinen, Anna Valldeoriola Cardó, and Aleksi Vartiainen.
The illustrations at the top of each chapter, and the similar drawings elsewhere in the ebook, are the work of Christina Lassheikki.
The animations that detail the execution Scala programs have been designed by Juha Sorva and Teemu Sirkiä. Teemu Sirkiä and Riku Autio did the technical implementation, relying on Teemu’s Jsvee and Kelmu toolkits.
The other diagrams and interactive presentations in the ebook are by Juha Sorva.
The O1Library software has been developed by Aleksi Lukkarinen, Juha Sorva, and Jaakko Nakaza. Several of its key components are built upon Aleksi’s SMCL library.
The pedagogy of using O1Library for simple graphical programming (such as Pic
) is
inspired by the textbooks How to Design Programs by Flatt, Felleisen, Findler, and
Krishnamurthi and Picturing Programs by Stephen Bloch.
The course platform A+ was originally created at Aalto’s LeTech research group as a student project. The open-source project is now shepherded by the Computer Science department’s edu-tech team and hosted by the department’s IT services; dozens of Aalto students and others have also contributed.
The A+ Courses plugin, which supports A+ and O1 in IntelliJ IDEA, is another open-source project. It has been designed and implemented by various students in collaboration with O1’s teachers.
For O1’s current teaching staff, please see Chapter 1.1.
Additional credits for this page
The idea for the question on dynamic scope came from a paper by Kathi Fisler, Shriram Krishnamurthi, and Preston Tunnell Wilson.
You may nest a function-calling expression within another.