Functions

A function is a named sequence of statements that performs a computation. To define a function, you specify the name and the sequence of statements. Later, you can “call” the function by name.

Function calls

We have already seen one example of a function, the function main() that exists in every Rust program. It kicks things off when you start your program, and it is the ”entry point” in programmer jargon.

The expression in parentheses is the argument of the function. The function main doesn’t accept any arguments. If you need to collect arguments from the environment, like from the command line, there are other ways to do it. But the function main should never accept arguments.

It is common to say that a function “takes” an argument and “returns” a result. The result is also called the return value.

It is important to declare what type the return value is. The compiler needs that information to check two things. First that the code follows the correct syntax and second, that it behaves as intended. The compiler will give you a helpful error message if you return something of the wrong type.

The main function should never return any value. You’ll find a lot of examples on the internet where main is set to return a result of the type Result. That’s because the author used function calls that might fail, and didn’t bother to deal with errors. Never let your main return anything. I’ll show you the right way to handle functions that might fail later.

Math functions

In Rust, each of the number types has math built-in, as functions on that type. For instance, lets say you need to raise 2 to the power of 3 (23). You can do that in several ways, depending on what type of number you are using:

let a = 2isize.pow(3);
let b = 2usize.pow(3);
let c = 2i32.pow(3);
let d = 2i64.pow(3);
let e = 2f32.powf(3);
let f = 2f64.powf(3);

Rust is very particular about types. You need to make sure that you use numbers of the same type when trying to use them in mathematical expressions. Take the simple operation of multiplying a floating point number with 2:

let g: usize = 2;
let h: f32 = 4.1 * g;

This will produce an error message. The compiler thinks we are trying to multiply a floating point number (4.1) with an integer (2). This is not allowed. Mixing number types has the potential to create bugs that can be very hard to find. We can fix it in one of two ways:

let g: f32 = 2.0; // g is now floating point
let h: f32  = 4.1 * g; // both numbers floating point works.

Another way, that lets us keep g as an integer:

let g: usize = 2;
let h: f32 = 4.1 * (g as f32) ;// convert g to f32

In this variant, we substitute the value g points to for its nearest equal in f32. but only on this line, without changing the original.

Wait, what does that ”its nearest equal” mean, isn’t it the exact same value but in floating point instead of integer? Well, not always. Converting between integers and floating points isn’t always exact. (That is true in most programming languages).

The different number types can do some tricks on their own too. For instance, they can do trigonometrical calculations on themselves. Like sin, asin and tan. Not to mention square roots and raising themselves to powers.

If we need to calculate the hypothenuse from the other two sides of a right angle triangle, we could do it like so:

let a: f32 = 3.0;
let b: f32 = 4.0;
let hypothenuse: f32 = (a.powf(2.0) + b.powf(2.0)).sqrt();
println!("{}", hypothenuse);

We need to tell the compiler what types we are using, hence the colon and type name after each variable name.

Then we call the function powf, that the type f32 has built-in, on both a and b to square them, and add the squares. We use parentheses around that sum, which is now a sum of two f32s. That means it is itself a f32 number, and we can call the function sqrt() on it, as that is also defined for f32.

A function that is attached to a type like this is a method on that type. Pretty much every type has methods defined on them, to let them perform various tricks for us.

To call a method on a type we add a period and then the method name and needed arguments. Like the a.powf(2.0) above. We can even use this dot notation on a parentheses, if the expression inside yields a value of the right type. Like we did in the example with (a*a + b*b).sqrt()

If we want to do more advanced math, the methods defined directly on the types we’re using might not be enough. Then we can use crates. A crate is like a “plugin” to add functionality in Rust. There are many crates for different purposes. Linear algebra, abstract algebra, statistical computing, 3D for games and graphics and more.

Before we can use the functions in a crate, we have to tell the compiler we want to use it.

In the Rust playground you can use the top 100 most downloaded crates from crates.io. You can also use all the crates from the Rust Cookbook, with all of their dependencies. To use a crate named foo, add the appropriate extern crate foo line to the beginning of the code.

Let’s look at an example where we need a random number generator:

extern crate rand;
use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    println!("{}", rng.gen::<u8>());
}

There is a lot going on here, so let’s break it down in smaller chunks.

The first line pulls in a crate called rand and makes it available to use in our program.

Then, on the second line, we tell the compiler that we actually will use it. This is crucial, because if we don’t, we won’t be able to use the functions inside that crate.

Then we have our main function, were we are going to use the crate.

We create a mutable variable called rng (short for random number generator). We point it to a method in rand, called thread_rng. The :: notation is special, and means that we want the compiler to look inside rand, to find thread_rng.

This special syntax means that we can use crates that contain things that have the same name. They still wont get confused, since we specify which crate they are in when we call on them.

On the next line we have the println! that we have used before, but this time it’s a bit different. It doesn’t contain a variable to print, there is something else going on here. In fact, two things we haven’t seen before.

Instead of a static variable we have a function call, to rng.gen(). And not only that, the function call looks weird too. It has a type declaration in it, <u8>. This means we want the function to generate a random number of type u8. So we will get a number between and including 0 and 255, and print that.

If you run this several times in the playground you should get a new result most of the times.

The notation function_name::<type_name>() even has a special name. The pattern ::<something> is a turbo fish. The name comes from the fact that ::<something> sort of resembles a fish.

We call putting calls to functions or macros inside a call to another function or macro composition. And that is what the next section is all about.

Composition

So far, we have looked at the elements of a program — variables, expressions, and statements — in isolation. It’s time we start talking about how to combine them.

One of the most useful features of programming is that you can take small building blocks and compose them. For example, the argument of a function. It can be any kind of expression, including arithmetic operators. Even a call to another function.

Almost anywhere you can put a value, you can put an arbitrary expression. There is only one exception: the left side of an assignment statement has to be a variable name. Any other expression on the left side is a syntax error.

Adding new functions

So far, we have only been using the functions that come with Rust, but it is also possible to add new functions. A function definition specifies two things. The name of the new function, and the sequence of statements that run when you call it. Here is an example:

fn print_lyrics() {
	println!("I'm a lumberjack and I'm okay");
	println!("I sleep all night and I work all day.");
}

fn is a keyword that indicates that this is a function definition. The name of the function is print_lyrics. The rules for function names are the same as for variable names. Letters, numbers and underscore are legal, but the first character can’t be a number. You can’t use a keyword as the name of a function, and you should avoid having a variable and a function with the same name.

Wait, what’s that, a keyword? What’s a keyword?

Every programming language has reserved words. You can’t use these as names of functions or variables. Rust has a list of current and possible future keywords that you’re not allowed to use. My advice: don’t try to memorize them. Look them over if you want to, but trust the compiler to yell at you if you try to use them.

Rust keywords

The empty parentheses after the name tell us that this function doesn’t take any arguments.

We call the first line of the function definition the header; and we call the rest the body. The header has to end with a { and we indent the body and end it with a }. By convention, indentation is four spaces. The body can contain any number of statements.

All quotation marks (single and double) must be “straight quotes”. Use the ", usually located next to Enter on the keyboard (US layout). “Curly quotes” like the ones in this sentence, are not legal in Rust.

Once you have defined a function, you can use it inside another function. For example, to repeat the previous refrain, we could write a function called repeat_lyrics:

fn repeat_lyrics() {
	print_lyrics();
	print_lyrics();
}

Then we can call that from our main function, making the whole program look like this:

fn main() {
	repeat_lyrics();
}

fn repeat_lyrics() {
	print_lyrics();
	print_lyrics();
}

fn print_lyrics() {
	println!("I'm a lumberjack and I'm okay");
	println!("I sleep all night and I work all day.");
}

But that’s not really how the song goes.

Definitions and uses

If we put all the code fragments from the previous section together, the program looks like this:

fn main() {
	repeat_lyrics();
}

fn repeat_lyrics() {
	print_lyrics();
	print_lyrics();
}

fn print_lyrics() {
	println!("I'm a lumberjack and I'm okay");
	println!("I sleep all night and I work all day.");
}

This program contains three function definitions:main, repeat_lyrics and print_lyrics. Function definitions get executed like other statements. But the effect is to create function objects. The statements inside the function do not run until you call the function. The function definition generates no output.

It doesn’t matter in what order you define the functions. The compiler will go through the entire file and pick up a list of all functions, before trying to compile it.

Flow of execution

Execution always begins with main(). Statements in main run one at a time, in order from top to bottom.

A function call is like a detour in the flow of execution. Instead of going to the next statement, the flow jumps to the body of the called function. Then it runs the statements there, and then comes back to pick up where it left off.

That sounds simple enough, until you remember that one function can call another. While in the middle of one function, the program might have to run the statements in another function. Then, while running that new function, the program might have to run yet another function!

Fortunately, Rust is good at keeping track of where it is. Each time a function completes, the program picks up where it left off in the function that called it. When it gets to the end of the program, it terminates.

In summary, when you read a program, you don’t always want to read from top to bottom. Sometimes it makes more sense if you follow the flow of execution.

Parameters and arguments

Some of the functions and macros we have seen need arguments. For example, when you call println! you pass a string as an argument. Some functions take more than one argument.

When you define your own functions, keep the arguments as few as possible. One is ideal, two acceptable. When you reach three you will start to have trouble remembering which order they should be in.

Inside the function, the arguments are assigned to variables called parameters. Here is a definition for a function that takes an argument:

fn print_twice(text: &str) -> () {
    println!("{}", text);
    println!("{}", text);
}

This function assigns the argument to a parameter named text. When we call the function, it prints two lines, each containing the text we gave it as a parameter.

We use the same rules of composition for our own functions, as we did for built-in functions. We can use any kind of expression as an argument for print_twice, as long as it evaluates to a &str:

fn main() {
    print_twice("Spam");
}

fn print_twice(text: &str) -> () {
    println!("{}", text);
    println!("{}", text);
}

Rust evaluates the argument before calling the function. In the example the &str sent to the function will be “Spam”, and the result will be

Spam
Spam

You can also use a variable as an argument:

fn main() {
    let my_text = "Spam";
    print_twice(my_text);
}

fn print_twice(text: &str) -> () {
    println!("{}", text);
    println!("{}", text);
}

The name of the variable we pass as an argument ( my_text ) has nothing to do with the name of the parameter ( text ). What the variable name was in the calling function doesn’t matter. In print_twice, we call every string we get as an argument text.

Variables and parameters are local

When you create a variable inside a function, it is local. This means that it only exists inside the function. For example:

fn cat_twice(text1: &str, text2: &str) {
    let concatenation = text1.to_owned() + text2;
    print_twice(&concatenation);
}

This function takes two arguments, and names them text1, and text2 respectively. When cat_twice terminates, both variables are destroyed.

Functions with return values and void functions

All the functions we have used, including print_twice, return results. In fact, in Rust, all functions do. Other languages have functions that perform an action but don’t return a value. They are called void functions.

In Rust, void functions don’t exist, all functions have a return value. Which means we lied above when we said that main() should never have a return value.

There are three ways the return value gets sent back to the caller:

  1. A line that starts with the keyword return and ends in a semicolon. The return value is that of whatever expression comes after the return keyword.
  2. If the last line in a function contains an expression without a semicolon at the end. The returned value will be that of the expression on that last line.
  3. If there is no such line, and no return statement, the return value will be what Rust calls the unit type, ().

If you plan to return something other than (), you must also declare what the type of that value will be. You declare the return type in the functions header:

fn print_twice(text: &str) -> () {
    println!("{}", text);
    println!("{}", text);
}

Here we first declare that we have an argument with a single value of type &str. We also declare that we will use a variable called text to point to that value inside the function.

We then declare that the return type is (). We do it by inserting an “arrow” with a “minus” and a “larger than” sign, ->, after the parentheses were the argument is declared. Then we write the name of the return type. Except when the return type is unit, then we either type () or just omit the “arrow” and the return type altogether:

fn print_twice(text: &str) {
    println!("{}", text);
    println!("{}", text);
}

When you call a function, you almost always want to do something with the return value. You might assign it to a variable or use it as part of an expression:

extern crate rand;
use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    println!("The winning lottery number is: {}", rng.gen::<u8>());
}

Functions that only return the unit type usually have side effects instead. They might display something on the screen or have some other effect. But their return value is usually useless.

They are also the only type of functions where we don’t need to declare a return type in the header. This also means that if we don’t declare a return type for a function, the compiler will assume that we return ().

This means that main always has a return value of (). Even though I said earlier that your main should never have a return value…

Why functions?

It may not be clear why it is worth the trouble to divide a program into functions. There are several reasons:

  • Creating a new function gives you a way to group and name several statements together. This makes your program easier to read and debug.

  • Functions can make a program smaller by eliminating repetitive code. Later, if you make a change, you only have to make it in one place.

  • Dividing a long program into functions allows you to debug parts of it one at a time. Then assemble them into a working whole.

  • Well-designed functions are often useful for many programs. Once you write and debug one, you can reuse it elsewhere.

Debugging

One of the most important skills you will learn is debugging. It can be frustrating. But debugging is one of the most intellectually rich, challenging, and interesting parts of programming.

In some ways debugging is like detective work. You get a bunch of clues and you have to figure out the processes and events that led to the results you see.

Debugging is also like an experimental science. Once you have an idea about what is going wrong, you change your program and try again. If your hypothesis was correct, you can predict the result of the modification. This takes you a step closer to a working program. If your hypothesis was wrong, you have to come up with a new one. As Sherlock Holmes pointed out, “When you have eliminated the impossible, whatever remains, however improbable, must be the truth.” (A. Conan Doyle, The Sign of Four)

For some people, programming and debugging are the same thing. That is, programming is the process of debugging a program until it does what you want. The idea is that you should start with a working program and make small changes, debugging them as you go.

For example, Linux is an operating system that contains millions of lines of code. But it started out as a simple program Linus Torvalds used to explore the Intel 80386 chip. According to Larry Greenfield, “One of Linus’s earlier projects was a program that would switch between printing AAAA and BBBB. This later evolved to Linux.” ( The Linux Users’ Guide Beta Version 1).

About the author

For the last three decades, I've worked with a variety of technologies. I'm currently focused on fullstack development. On my day to day job, I'm a senior teacher and course developer at a higher vocational school in Malmoe, Sweden. I'm also an occasional tech speaker and a mentor. Do you want to know more? Visit my website!