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.7: Creating Custom Functions
Introduction
In this chapter, we’ll revisit some of the functions that we already discussed and introduce some new ones. But what’s really new here is that now we’ll be taking a look at the program code that implements the functions.
To experiment with these functions, start up your REPL as you did in the previous chapter: make sure you have the Subprograms module selected in the Project view as you hit Ctrl+Shift+D. (An alternative way is to open a relevant code file from Subprograms in the editor and then launch the REPL.)
The program code for the chapter is available in o1/subprograms/examples_ch17.scala
within
the module, if you want to take a look.
Defining a Function
The average
function is familiar to us from Chapter 1.6. We can use it in the REPL as
shown here:
average(2.0, 5.5)res0: Double = 3.75
In order for that command to work, the averaging function needs a definition. This example function’s definition is fairly simple but nevertheless introduces a number of important concepts.
In English, the function’s operating principle is this: “When the function average
is
called, it produces a return value by adding the value of the first parameter to the
value of the second parameter and dividing the sum by two.” This principle is captured
in Scala below.
(Side note: From here on in, our examples will gradually grow in complexity, and many of them will be displayed in boxes such as this one, with different parts of the program highlighted in different colors. As you will have noticed, IntelliJ does something similar albeit in different colors.)
def average(first: Double, second: Double) = (first + second) / 2
We state: “when calling the function average
, one must pass
two Double
values as parameters”. Speaking more generally,
this is where we define the function’s parameter variables
(parametrimuuttuja; also known as formal parameters) and
their data types.
Each parameter variable has a name. In our example, we’ve chosen
simply to call the first parameter variable first
and the second
second
.
Note the equals sign, which is followed by...
... the actual implementation of the function, known as the function body (funktion runko). The function body implements the algorithm that accomplishes whichever task the function is intended to accomplish. In our simple example, the body is just a single arithmetic expression that instructs the computer what it needs to do to obtain the return value.
The parameter variables first
and second
genuinely are variables — val
variables,
to be more specific. Parameter variables don’t receive values from an assignment command
but indirectly through function calls; apart from that, though, you can use them just
like the other variables that you’ve seen.
Function Execution and the Call Stack
The following animation shows another diagram of data in computer memory. The diagram evolves step by step as the program runs. This time, we’ll also inspect what happens within a function: you’ll see how parameter variables work, for instance. Examine this animation thoroughly! It introduces concepts that play crucial roles in the programs that we’re about to write.
The piece of code illustrated in the animation doesn’t accomplish anything much. It just calls the averaging function a couple of times.
The animation featured frames (kehys). The frames formed a call stack (kutsupino), which is often known simply as the stack and which indeed works like a real-world stack of items: when a function call begins, a new frame is added to the top of the stack, and when a call terminates by returning a value, that topmost frame gets removed from the stack. The computer uses the frames of the call stack to keep track of which function calls are active and what data is associated with each of those calls. At any given time, it’s the topmost frame of the stack that is active, while the lower frames (of which there was just one in this animation) wait for the frame above to finish its task before resuming theirs.
Hopefully, the animation already helped you make sense of how a function call unfolds. Be that as it may, the significance of the call stack and its frames will soon be highlighted as we take on some more complex functions.
A function call... is what exactly?
Chapter 1.2 mentioned that we use the word “program” in reference to static program code (as in “The program has 100 lines.”) but also in reference to the dynamic processes that take place when programs are run (as in “The program stored the information entered by the user.”).
“Function call” is similarly ambiguous. Sometimes we mean an expression written in code: “The function call on line 15 has a punctuation error.” And sometimes we refer to processes like the one in the preceding animation; those processes take place when a function is executed: “The function call is over and yielded a return value of 4.”
Be wise to both meanings as you go through this ebook and other resources on programming.
Practice Defining a Function
Programming Exercise: meters
Task description
In the United Kingdom and some of its former colonies, feet and inches are still widely used for measuring length. One inch equals 2.54 cm, and one foot is 12 inches. Measurements are commonly expressed as a combination of these units. For instance, a person who is about 180 cm tall might be described as “five foot eleven”: five feet plus eleven inches.
Implement an effect-free Scala function that:
is called
toMeters
;takes in, as its first parameter, a number of feet represented as a value of type
Double
;similarly takes in a number of inches as its second parameter; and
computes and returns a
Double
equal to the total length in meters of the given feet and inches (e.g., given the values 5 and 11, returns 1.8034).
Proceed in the following stages: set up the assignment, write some code, test. Keep adjusting and testing your program until it works.
There are some additional instructions for each stage below.
Setup
In IntelliJ, find the Subprograms module and its file o1/subprograms/week1.scala
.
Near the top of the file, there is a comment that shows you where you’ll be writing your
code as you work on this chapter.
Writing your function
First, make sure you understand precisely what the function is supposed to do! This sounds self-evident, sure, but it’s easy to forget. You can lay substantial pain upon yourself just by overlooking something simple in the task description.
Write the function definition in the indicated section of the file. In this case, one line is enough.
Start with
def
and the name of the function.Type in the parameter definitions. Choose sensible names for the parameter variables.
Write the function body. A single arithmetic expression will do to convert between the units. (Don’t print out the resulting value; simply return it!)
Error messages in IntelliJ
As you edit your code, you’ll see some saw-toothed highlights and other annotations in red. Some of your code may also turn red. This is IntelliJ’s way of letting you know that it has spotted problems.
Don’t be flustered if your program lights up in red as soon as you start writing. IntelliJ adjusts these annotations constantly as you type, and incomplete code often prompts a complaint. For instance, when you’ve typed in just the word
def
and a name, IntelliJ will cry out, noticing that your function definition is incomplete.The red highlights can be a great help. You should take them seriously, but only if they remain after you consider your
toMeters
function to be ready.You can view more detailed error messages by hovering your mouse cursor over the red highlights. Unfortunately, it can be hard to understand what each error message means if you’re going by just what we’ve covered in O1 so far; at this stage, you may find it easier to fix your code by comparing it to the ebook’s examples.
Take care that no red error annotations remain. In particular, eliminate any errors in punctuation. Did you use at least round brackets, a comma, and an equals sign? Were you consistent in how you wrote the name of each variable?
Compiling your code
Before the computer can run your Scala code (in the REPL or otherwise), it needs to compile (kääntää) the code into a form that it can execute. As it compiles your code, the computer also thoroughly checks that it follows the rules of the programming language. That compilation-time check may produce errors that you need to fix before your code can run.
IntelliJ compiles your code automatically whenever you launch a program either for the first time ever or after modifications. You can also ask IntelliJ to compile your program at any time, to make sure that there are no automatically detectable errors. Let’s try that now.
In IntelliJ’s menu, select Build → Build Module 'Subprograms' or press the corresponding shortcut key F10. (In IntelliJ, the command for compiling your code is known as building, which word refers generally to preparing a program for use.)
After a moment, one of the following happens.
Either:
You don’t get any error messages, only a barely noticeable text at IntelliJ’s bottom-left corner: Build completed successfully. If this happened, great, but do make an intentional little formatting error in your code now and press F10 again, just to see the other possible outcome.
Or:
The Build tab pops up with one or more error messages. This list is generally similar to what accompanies the red highlights in the editor (but may be more comprehensive). You’ll need to fix your code. After fixing it, press F10 again.
Fixing errors and compiling your code
Before running a program you wrote, always make sure you have gotten rid of the errors highlighted by IntelliJ. The next step of testing your code will surely fail if you don’t!
IntelliJ is quite clever and notices many errors that prevent the code from running “in advance”, immediately as you type code into the editor. This is not foolproof, however, and some errors only show up when you compile the entire code.
Since IntelliJ compiles your code automatically when you run it — such as when you launch the REPL — it’s not necessary that you expressly ask IntelliJ to compile. But it’s very easy to just hit F10 whenever you feel like it and so have your code’s grammaticality checked.
Testing your function
Start a REPL session with Ctrl+Shift+D, making sure that the Subprograms module loads up there. Note that in order to test your new code, you need a fresh REPL session.
Try calling your function on different parameter values. Does it work right? If not, return to Step 1 of Writing your code above. (Yes, to Step 1, not straight to Step 2.) Ask the O1 staff for help as needed.
(P.S. Did you notice that the result is expected in meters, not in centimeters?)
On “rebooting” the REPL
Whenever you edit your program and wish to test the new version in the REPL, you need to load the new code into the REPL. You can either close the REPL and start it again or press the Rerun icon in the REPL’s top left-hand corner. If you then wish to re-enter some of your earlier commands, remember that you can access them with the arrow keys.
Submit your solution
When you’re confident that your function works, enjoy the moment and submit your solution via IntelliJ.
A+ presents the exercise submission form here.
More simple functions
If you wish, you can build some programming routine with this voluntary and easy practice task that greatly resembles the previous assignment.
In the same file, week1.scala
, write the following functions:
volumeOfCube
, which expects a single parameter that represents a cube’s edge length and returns the cube’s volume. The parameter and the return value are bothDouble
s.areaOfCube
, which similarly computes and returns a cube’s area, given the cube’s edge length. (That is, the function returns the area of the whole cube not just one of the cube’s sides.)
A+ presents the exercise submission form here.
Defining Functions within the REPL
In the previous assignment, you used IntelliJ’s editor to write and save your functions in a separate file. That’s what we’ll be doing in upcoming assignments as well. However, it’s good to realize that you can also enter function definitions straight into the REPL:
def average(first: Double, second: Double) = (first + second) / 2average(first: Double, second: Double): Double average(2, 1)res2: Double = 1.5
This can be quite convenient when experimenting. Howevever, using IntelliJ’s editor is better preparation for future assignments and makes for convenient A+ submissions. Saving files in the editor also lets you keep the code you wrote.
As you experiment with Scala commands on your own, feel free to define functions in the REPL.
Practice on Functions and Modulo
The modulo operator
In addition to the everyday arithmetic operators, programmers commonly employ the modulo
operator, which is written in Scala as %
. This operator is usually associated with
integer numbers; one might evaluate the expression 25 % 7
, for example.
Now go and experiment with the modulo operator on your own in the REPL.
Example: modulo and parity
For various reasons, we may find it useful to determine a number’s parity: whether it is even or odd. We might, for instance, wish to do something to every other element of a list, such as coloring the even and odd rows of a large table in a different background colors. Right now, we’re going to use parity simply as an additional example of defining a custom function and applying the modulo operator.
Let’s create an effect-free function that returns zero to signal that a given number is even. Otherwise, it will return either 1 or -1, matching the sign of the given number. Like so:
parity(100)res3: Int = 0 parity(2)res4: Int = 0 parity(103)res5: Int = 1 parity(7)res6: Int = 1 parity(-7)res7: Int = -1
The function is easily implemented with the modulo operator: if we divide a number by two, we get a remainder of zero if and only if the number was even. Here’s a Scala implementation:
def parity(number: Int) = number % 2
Go ahead and define the function and experiment with it in the REPL, if you want. Or continue right away to the next assignment.
Chess boards: introduction
Let’s say we’ve numbered the squares on a chess board from 1 to 64, starting at the top left and progressing from left to right one row at a time. Moreover, we have numbered the rows and columns (ranks and files in chess lingo) from 1 to 8 each. There’s a picture right there.
If we know the number of a square, such as 35, how do we get the computer to work out which row and which column the square is at? One possibility is to define two effect-free functions that can be used as follows.
row(35)res8: Int = 5 column(35)res9: Int = 3
Here are the function definitions:
def row(square: Int) = ((square - 1) / 8) + 1
def column(square: Int) = ((square - 1) % 8) + 1
Chess boards: an assignment
But what if we had instead numbered the squares from 0 to 63 and the rows and columns
from 0 to 7, as shown in this picture? Write two effect-free functions, row
and
column
, that work like this:
row(34)res10: Int = 4 column(34)res11: Int = 2
Instructions and hints:
Use the same workflow as in the
toMeters
assignment.These functions, too, should go in
week1.scala
.If you understand the given versions above, where numbering runs from one upwards, this isn’t a difficult assignment. Under the zero-based numbering scheme, the solution is actually simpler.
Test your code in the REPL before you submit.
A+ presents the exercise submission form here.
Consecutive Commands in a Function
Just to try out something, let’s create a function that prints out the same four lines
of text every time it’s called, followed by a fifth line that the function’s caller must
pass in as a parameter. We intend to be able to use our function, careerAdvicePlease
,
like this:
careerAdvicePlease("I don't know what to do.")Oppenheimer built the bomb, but now he's dead. (Dead!) Einstein was very, very smart, but not enough not to be dead. (Dead!) So don't go into science; you'll end up dead. Don't go into science; you'll end up dead. I don't know what to do. careerAdvicePlease("Such is the human condition.")Oppenheimer built the bomb, but now he's dead. (Dead!) Einstein was very, very smart, but not enough not to be dead. (Dead!) So don't go into science; you'll end up dead. Don't go into science; you'll end up dead. Such is the human condition.
Here is the function definition:
def careerAdvicePlease(addendum: String) =
println("Oppenheimer built the bomb, but now he's dead. (Dead!)")
println("Einstein was very, very smart, but not enough not to be dead. (Dead!)")
println("So don't go into science; you'll end up dead.")
println("Don't go into science; you'll end up dead.")
println(addendum)
The body of careerAdvicePlease
contains multiple commands in
sequence. In cases like this, we place the commands on consecutive
lines. What this means is that each of the five println
commands
gets executed, in order, whenever careerAdvicePlease
is called.
This is also our first proper example of a code construct that is split over multiple lines. We indent (sisentää) the lines that are contained within the definition and make up the function body.
What about the return value?
Our careerAdvicePlease
function returns the contentless Unit
value, or “returns
nothing”, so to speak. This is because the command in the function body that is
executed last determines the return value of the entire function. In our example,
the last command to be executed is the last of the println
s, which yields only a
Unit
value (as discussed in Chapter 1.6). Therefore, the same Unit
value is
also returned by the careerAdvicePlease
function that calls println
.
Line Breaks and Indentations in Functions
We just saw that a function definition may be split across multiple lines, with some of them indented. This is more important than it might seem at first blush. Let’s take a closer look.
The importance of indenting right
Indentations are common to many programming languages. In some languages, indentations are completely optional and don’t impact on a program’s behavior, but it is nevertheless considered good style to indent one’s code, because the indentations bring out the program’s structure and thus assist readers. In other languages, indentations genuinely affect a program’s structure and behavior.
Scala 3 falls in the second group of languages: indentations aren’t just a matter of style. Our earlier code wouldn’t work if we indented it haphazardly, like this for instance:
// This doesn’t work.
def careerAdvicePlease(addendum: String) =
println("Oppenheimer built the bomb, but now he's dead. (Dead!)")
println("Einstein was very, very smart, but not enough not to be dead. (Dead!)")
println("So don't go into science; you'll end up dead.")
println("Don't go into science; you'll end up dead.")
println(addendum)
Language versions are different
The above text applies to Scala’s modern version 3, which is what we use. In earlier versions of the language, indentations were optional. However, you had to write additional pairs of brackets around function bodies, and it was customary to indent the code anyway.
You may run into old-school Scala on websites or in dated books. There’s a bit more about this at the end of this page and in our style guide.
Indentation size
In Scala, it’s customary to use indentations that are two spaces wide, as in our original function and in upcoming examples. But it’s possible to use a different indent width as long as you’re consistent. For example, here the indentations are seven spaces wide and the code works fine despite its unconventional style:
// Works, but this style is a bit odd.
def careerAdvicePlease(addendum: String) =
println("Oppenheimer built the bomb, but now he's dead. (Dead!)")
println("Einstein was very, very smart, but not enough not to be dead. (Dead!)")
println("So don't go into science; you'll end up dead.")
println("Don't go into science; you'll end up dead.")
println(addendum)
If you feel like a two-space indent is too narrow for optimal readability, feel free to adopt an indent width of four spaces, for example.
When should I indent?
In the careerAdvicePlease
function, we split the code onto multiple indented lines since
we wanted to group multiple print commands into the same function body. There is in fact
nothing preventing us from using line breaks and indentations in smaller functions, too.
For instance, both these implementations of average
do the same thing.
Version 1 (no line breaks):
def average(first: Double, second: Double) = (first + second) / 2
Version 2 (line break and indent):
def average(first: Double, second: Double) =
(first + second) / 2
Many Scala programmers follow these conventions:
If the function body contains multiple commands in sequence, write the line breaks and indent the body. This is what we did in
careerAdvicePlease
and Version 2 above.Also, if the function is effectful (e.g., it prints out something), break the lines and indent as in Version 2, even if the function body consists of just one line.
However, if the function body consists of a single line that is effect-free (Chapter 1.6 and see below), you may adopt the one-liner style. Version 1 is therefore a fine way to write
average
. (However, if the line becomes excessively long, it’s probably still better for readability to break it after the equals sign.)
To summarize: the style in Version 1 is appropriate only if the function definition is a single, effect-free line of code that is not terribly long.
In this ebook, we generally follow those style conventions. We recommend that you do the same. You may find the conventions confusing at first, but don’t let that bother you too much when writing your own programs. If it feels easier for you, go ahead and format all your functions as in Version 2. It’s always okay to break the line and indent the body!
How do I know if my function is effectful?
Imagine the scene. Your program is running and is in a particular state: certain things have been stored in memory and something is displayed onscreen. A function is then called. It does its job and produces a return value; a bit of time passes. Apart from that, is the program back in the state where it was before the function call?
If the called function is, say,
average
, we are effectively back to where we were: we obtained a result but nothing else changed. This is an effect-free function.If the called function is, say,
println
, we aren’t back to the original state: the program has generated a line of output. This is an effectful function.
Here’s another way to think about it: if you knew the return value of the function
already, would it even be necessary to call the function? In the case of our effect-free
averaging function, the answer is no: we could have just as well replaced the call
average(5, 10)
with the literal 7.5
. In the case of the effectful println
, on
the other hand, we don’t get any printing done without calling the function.
You may mark where a function body ends
Sometimes you may want to be extra clear about where a function’s code ends. Scala provides you with a tool for this: you can write an end marker (loppumerkki) at the end of a function body. Our earlier example functions could also be written like this:
def average(first: Double, second: Double) =
(first + second) / 2
end average
def careerAdvicePlease(addendum: String) =
println("Oppenheimer built the bomb, but now he's dead. (Dead!)")
println("Einstein was very, very smart, but not enough not to be dead. (Dead!)")
println("So don't go into science; you'll end up dead.")
println("Don't go into science; you'll end up dead.")
println(addendum)
end careerAdvicePlease
An end marker begins with the word end
. When we’re ending
a function definition, like here, we follow end
with the
function’s name. Note that the end markers aren’t indented
deeper but aligned with the corresponding def
s.
The purpose of end markers is not to modify the program’s behavior but to assist human readers. But most of the time you don’t really need an end marker to tell where a function ends, so people often don’t write these markers on functions. In this ebook, too, we’ll usually not write them on functions, although you’re free to do so if you prefer.
End markers do have other uses, too, which is something we’ll discuss later.
Assignment: Text Accompanied by Music
Task description
Write an effectful function called accompany
that prints out some text and accompanies
it with sound. The function should take two parameters, each of which is a string: the
function prints out the first string and plays the second string.
When you’re done, it should be possible to use the function as in this example.
accompany("Nananana nananana nananana nananana BATMAAN!", "[49]<" + "(d<g>)(d<g>)(db<g>)(db<g>)(c<g>)(c<g>)(c#<g>)(c#<g>)" * 2 + "[54]>(FG)-.(FG)---/160")Nananana nananana nananana nananana BATMAAN! accompany("... and introducing acoustic guitar", "[26] >EF#A---G#---F#---E---F#G#---------EF#A---G#---F#---E---F#EF#---------/192")... and introducing acoustic guitar
The line breaks in the example are there just to prevent overlong lines. They aren’t strictly necessary.
Instructions and hints
Write your solution in the same file,
week1.scala
.Pick sensible names for the parameter variables.
In this assignment, the order in which you call
println
andplay
within the function body has no practical significance. Take your pick, as long as you do both.The
play
function has been defined so that it plays the notes in the background and the computer doesn’t wait for the sound to finish before executing the next command. So the printout will appear promptly even if you place theplay
command before theprintln
.
Follow the guidelines on programming style given above and split your code over multiple lines. Remember to punctuate.
A+ presents the exercise submission form here.
Tools for Building New Functions
The preceding examples have shown that when you implement a function body, you can apply
the programming techniques that you know from earlier chapters: arithmetic operators,
println
, play
, etc. Other familiar commands work, too. For instance, a bit further
down in this chapter, we’re going to define new variables within a function. Throughout
O1, you’ll learn additional tools for building your own functions: you’ll be able to
make functions choose between alternatives, execute commands repeatedly, and so on.
One common thing to do in a function is to call another function. For instance, we can
use the hypotenuse function from package scala.math
to create a function that determines
the distance between two points (x1,y1) and (x2,y2):
def distance(x1: Double, y1: Double, x2: Double, y2: Double) = hypot(x2 - x1, y2 - y1)
The picture functions from package o1
, introduced in Chapter 1.3, are also available to
us as we create new functions:
A picture-creating function
This function produces red “balls” of different sizes:
def redBall(size: Int) = circle(size, Red)
A usage example:
val ball = redBall(50)ball: Pic = circle-shape val biggerBall = redBall(300)biggerBall: Pic = circle-shape show(biggerBall)
Vertical bars
In week1.scala
, create a function called verticalBar
that forms pictures of
blue rectangles that are ten times as many pixels high as they are wide.
The function should take a single parameter of type Int
, namely the width of the bar.
Here’s a usage scenario:
val picOfBar = verticalBar(80)picOfBar: Pic = rectangle-shape show(picOfBar)show(verticalBar(180))
Give a meaningful name to the parameter variable. Use show
to test your function in the
REPL. Please note that your function must not call show
and display the picture; it must
return the picture. Then you’ll be able to use your function and show
in combination
as illustrated above.
You can return to Chapter 1.3 for a recap on how to create shapes.
A+ presents the exercise submission form here.
An overloaded function
Continue from the previous assignment by creating another verticalBar
function. That
is to say, do not erase the original you just wrote but add another function with the
same name. This new function should take two parameters, a width (Int
) and a color
(Color
). It should be usable like this:
val blackBar = verticalBar(80, Black)blackBar: Pic = rectangle-shape show(blackBar)
Clearly, this second verticalBar
function will be more flexible (more abstract) as far
as color is concerned. On the other hand, it requires its caller to pass in an additional
parameter. This second function, too, returns a picture that’s ten times as high as it’s
wide.
A+ presents the exercise submission form here.
As you see, Scala allows us to define multiple functions with the same name, even in the same context. This is called overloading (kuormittaa) the function name. Overloading is allowed only if the functions that share a name differ in which parameters they take.
Despite their shared name, overloaded functions (here: the two verticalBar
functions
you wrote) are completely distinct from each other. When you use the functions’ name, the
parameter expressions that you provide determine which function gets called.
Practice Understanding Functions
Local Variables
Example: tax rates
Let’s write an effect-free function that calculates how much income tax one has to pay under this simple system of taxation:
Until a specific threshold income, one pays a base percentage of one’s income as tax (e.g., 20 percent), as set by the government.
In addition, one must pay a higher percentage (e.g., 40 percent) of all that one earns above the threshold (if anything).
When invoking our function, incomeTax
, we’d like to pass in the earner’s total income,
the threshold income, the base rate of taxation, and the additional rate, as shown below.
incomeTax(50000, 30000, 0.2, 0.4)res12: Double = 14000.0 incomeTax(25000, 30000, 0.2, 0.4)res13: Double = 5000.0
It would be possible to define this function as a one-liner, whose body consists of a single expression that evaluates to the function’s return value. But we can do better than that. Let’s store intermediate results in variables so that our code is a bit neater.
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
We define two local variables (paikallinen muuttuja) and assign intermediate results to them.
Recall that when a Scala function consists of multiple commands, the last command determines the return value of the function. Our example function returns the value of the arithmetic expression on the last line. The two lines above it set up that final expression.
Local variables are accessible only within the program code of the function itself.
A function’s caller doesn’t need to be aware of the called function’s local variables,
nor could the caller use them even if they were so aware. Any user of incomeTax
, for
instance, can remain blissfully unaware of what the function’s local variables are called
and indeed unaware that the function has any local variables to begin with. And the
following attempt to tamper with the function’s internal variables is unsuccessful,
irrespective of whether we have previously called incomeTax
.
additionalTax * additionalRate-- Error: |additionalTax * additionalRate |^^^^^^^^^^^^^ |Not found: additionalTax
As a matter of fact, the local variables don’t even exist during the entirety of the program run but only while the function is being executed. Local variables are created within the frame associated with the function call on the call stack. As soon as the function call is over, any memory reserved for the variables is released for other use. This is depicted in the animation below.
More function-reading practice
Exercise: Course Grades
Task description
Create an effect-free function that determines a student’s overall course grade according to a specific course policy. In our imaginary example course, the overall grade is the sum of a project grade (between 0 and 4), a bonus from an exam (0 or 1), and a bonus for active participation (0 or 1). However, the overall grade can never exceed 5.
Your function must adhere to the following specification.
Its name is
overallGrade
.As parameters, it expects three integers: a project grade and the bonuses for exam and participation, in that order.
It returns the corresponding overall grade between 0 and 5, also as an integer. (It doesn’t print the grade but returns it.)
You must ensure that even if the sum of the components is more than five, the overall grade isn’t. However, you don’t need to concern yourself with what happens if someone were to call your function on invalid parameter values (such as a negative project grade or an excessive exam bonus).
Instructions and hints
Once again, write your code in week1.scala
.
Follow the now-familiar workflow recapped below.
Make sure you understand precisely what the function is supposed to do!
Implement the function in stages: name, parameter variable (with sensible names!) and their types, function body.
Reset your REPL.
Test your function on a variety of parameter values. Go back to Step 1 if needed.
Submit your program only when you’re satisfied that it works.
Chapter 1.6 introduced a mathematical function that we already used in this chapter
too and that you’re likely to find useful. (If you experiment with those functions in
the REPL, remember to import scala.math.*
. That import
already appears at the top
of week1.scala
, so the math functions are available in that file.)
A+ presents the exercise submission form here.
The package
Keyword
The Scala file that you’ve edited begins with the word package
, like this:
package o1.subprograms
The meaning is fairly self-evident: the package
keyword is used at the top of each
Scala file to mark which package the contents belong to.
These package
definitions are necessary but unnoteworthy. They’ve been largely omitted
from the code fragments in this ebook’s text. They are included in the IntelliJ modules,
though; during O1, you’ll see these definitions at the top of many files.
Good to Know: Versions of Scala
In this chapter, we’ve written code that looks like this:
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
That’s the sort of code that we’ll keep writing, too. However, if you explore the Scala pages out there on the internet, you’ll also run into code that looks like this:
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
}
The difference here is the curly brackets around the function body. You may run into other differences as well.
This has to do with language versions. In O1, we’re using the up-to-date version of the language, Scala 3, which was released in 2021. There are many websites and books out there that don’t. In older versions of Scala, one had to write curly brackets in a lot of places, and there were some other differences in syntax too. Our style guide says a few more words about this; we recommend that you read that guide at some point reasonably early in O1, but not necessarily now. Around Week 3 would be good, for example.
Summary of Key Points
A function definition includes the function’s name, its parameter variables and their data types, and a function body. All in all, it is an implementation for an algorithm that takes care of whatever the function is expected to do.
When a function call starts, the computer reserves a so-called frame of memory to track the call.
The parameter values received by the function are stored in variables within the frame.
You can also define additional variables in the frame. Such a variable is known as a local variable.
Frames form a call stack (which we’ll discuss further in the next chapter).
Links to the glossary: function, function call, function body, end marker; frame, call stack; local variable, parameter variable, to overload.
The diagram below includes a handful of concepts that are central to how functions work on the inside.
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 “career advice” comes from The Arrogant Worms.
A function definition begins with the
def
keyword followed by a name for the function, as chosen by the programmer. In Scala, it’s customary to start function names with a lower-case letter, just as we did earlier with variables.