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

../_images/person01.png

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

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.

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

Let’s define a simple effect-free function that can be used like this (once the function is defined).

yell("Yippee")res1: String = Yippee!
val result = yell("Muhaha")result: String = Muhaha!
val louder = yell(result)louder: String = Muhaha!!

Here’s a template for the function definition. Five parts have been “left blank” and marked with three question marks each.

??? ???(phrase: ???) = ??? + ???

Fill in the blanks: replace the question marks to define a yell function that can be used as in the REPL example. Each of the five blanks calls for a different code fragment. You can model your answer on the average function shown above.

Enter the yell function’s full definition here. (That is, write the above line with the question marks replaced with proper code.)

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

  1. 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.

  2. Write the function definition in the indicated section of the file. In this case, one line is enough.

    1. Start with def and the name of the function.

    2. Type in the parameter definitions. Choose sensible names for the parameter variables.

    3. 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 error_underline 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.

  3. 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

  1. 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.

  2. 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 rerun 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 both Doubles.

  • 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.

Which of the following options best describes what the modulo operator does?

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

../_images/shakki1.png

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

../_images/shakki2.png

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 printlns, 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 defs.

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 and play 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 the play command before the println.

  • 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

For each function, select the options that correctly describe it.

Here’s the first function:

def distance(x1: Double, y1: Double, x2: Double, y2: Double) = hypot(x2 - x1, y2 - y1)
def redBall(size: Int) = circle(size, Red)
def experiment1(number: Int) =
  println("The number is: " + number)

In the next function, notice the type of the parameter variable: the caller must provide a reference to a buffer that contains integers.

def experiment2(numbers: Buffer[Int]) =
  numbers(0) = 100
def experiment3(number: Int) =
  println("The number is: " + number)
  number + 1

Which of the following best describes what happens when a function call is made (as per the model of program execution introduced in this chapter)? Review the animations at the beginning of the chapter, if necessary.

As an additional exercise, see if you can envision a scenario where it makes a difference when the parameter expressions are evaluated: whether evaluation happens before the function call starts or only during the function call.

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

Consider how local variables were represented in the preceding animation. Then take your best shot at assessing this claim: “Parameter variables are local variables, too.”

For each function below, select the options that correctly characterize the function.

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
def experiment4(word: String) =
  var number = 1
  println(word + ": " + number)
  number = number + 1
  println(word + ": " + number)
  number = number + 1
  println(word + ": " + number)
  number
def experiment5(initial: Int) =
  var number = initial
  number = number + 1
  number = number + 1
  number = number + 1
  number

In Chapter 1.6, you used a function called canon. Let’s undertake to create a function that is somewhat simpler in its implementation but similar in purpose. Our new function should play a given melody on two instruments, using one instrument after the other has finished (not overlapping as with canon). Here’s how we intend to use it:

val duet = onTwoInstruments("g#e--f#c#----", 26, 66, 5)duet: String = [26]g#e--f#c#----     [66]g#e--f#c#----
play(duet)

The function returns a string that contains two copies of the melody specified by the first parameter.

The second and third parameters indicate the dueting instruments. Our function uses these parameters to insert the appropriate markup into the string that it returns, such that play will interpret them as an instruction to play the melody first on one instrument, then on the other.

The last parameter defines the length of the pause between the two instruments.

Consider this implementation of onTwoInstruments, which you can also find in week1.scala. It almost works.

def onTwoInstruments(melody: String, first: Int, second: Int, lengthOfPause: Int) =
  val melodyUsingFirst  = "[" + first  + "]" + melody
  val melodyUsingSecond = "[" + second + "]" + melody
  val pause = " " * lengthOfPause
  val playedTwice = melodyUsingFirst + pause + melodyUsingSecond

That implementation contains a typical beginner’s mistake. Inspect the code carefully; you can also try calling it in the REPL. Among the options below, choose all that apply. If you want, you can also fix the function’s code within the module.

How can the bug be fixed? Select one or more of the claims below, as appropriate. If you want, you can also fix the function’s code within the module.

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.

  1. Make sure you understand precisely what the function is supposed to do!

  2. Implement the function in stages: name, parameter variable (with sensible names!) and their types, function body.

  3. Reset your REPL.

  4. Test your function on a variety of parameter values. Go back to Step 1 if needed.

  5. 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 drop of ink
Posting submission...