2.3 Functions

Functions are used for many purposes—whether it be to reuse an algorithm multiple times throughout your code, or split a program into multiple parts—all in order to simplify and organize you projects.

You may remember the main function from our previous examples throughout this tome—such a function is the most basic a function can get and still be useful, whilst also functioning correctly.

Perhaps you’ve missed me—says the function:

fn main()
{
    println!("Hello, world!");
}

As stated on a previous occasion—the code block right after the function definition acts as a bundle for what a certain function contains.

Other functions

New functions may be defined wherever you see fit. These can in turn be called upon from other places such as your main function:

fn main()
{
    foo();
}

fn foo()
{
    println!("foo!");
}

This program will start at the main function—and as the first thing it does run the foo function, which outputs “foo!” into the console.

One could even take this one step further and make foo run another function:

fn main()
{
    foo();
}

fn foo()
{
    println!("foo!");

    bar();
}

fn bar()
{
    println!("bar!");
}

This will first enter the main function, which runs the foo function. The foo function prints “foo!” into the console, followed by running the bar function—which prints “bar!”:

foo!
bar!

The bar function could in turn call another function—that too could call another function, continuing enlessly into the depths of the Junivörs, seemingly without a set goal.

Arguments

Perhaps plain functions aren’t too useful, since we can’t really tell them what we wish them to do. Let’s create something slightly more interesting:

fn main()
{
    print_num(5);
    print_num(7);
    print_num(3);
}

fn print_num(x: i32)
{
    println!("This number is {}!", x);
}

This code makes use of a function called printnum to simplify printing out numbers. Notice how printnum has the text x: i32 inside of its place for parameters—this tells us that print_num has one parameter—x by the type of i32. When creating functions you need to specify the type of the variables using type annotations—otherwise Rust wouldn’t have been able to guarantee our program’s safety.

The above example will print as:

This number is 5!
This number is 7!
This number is 3!

Since we can call functions an unlimited amount of times, it doesn’t matter how many times we call it—the resulting program will contain all the function calls, thus leaving us with three lines of text considering we called print_num three times.

Multiple arguments

We can define our function to take multiple arguments, seperating each one with commas ‘,’:

fn main()
{
    battleship_coord(5, 3);
    battleship_coord(7, 6);
    battleship_coord(3, 8);
}

fn battleship_coord(x: i32, y: i32)
{
    println!("There’s a battleship at ({}, {}).", x, y);
}

Do make sure to specify the type of every single on of the parameters inside the battleship_coord place for parameters.

The above example will print this out into the console:

There’s a battleship at (5, 3).
There’s a battleship at (7, 6).
There’s a battleship at (3, 8).

And so you are at it sinking battleships again!

If you were to enter a variable binding into a function, the function would take ownership of that variable—this will be discussed in the section on Ownership. Given you don’t yet know what this means, hold your horses and wait to provide functions with variable bindings until you know about ownership! It can on the other hand be said that it’s okay to provide bindings to functions as long as it is a numeric type, as these implement a certain kind of trait called Copy. Traits are discussed over at the Traits section.

Returning

Functions always return a value back to the place it was called from. In the aforementioned examples, we simply get no usable value, but can easily make a function which is specified to return something more useful—say an integer—to the place it’s called:

fn main()
{
    let x = foo();

    println!("Foo returned {}.", x);
}

fn foo() -> i32
{
    32
}

The little arrow-like piece of text -> to the right of the foo function specifies the return type this specific function has.

We’re creating a variable binding by the name of x, which is bound to the value of whatever the function foo decides to return unto us—we do however know that this value is sure to be of type i32.

By putting a valid value corresponding to a function’s return type—at the end of the the same function—that value will be returned unto the place where the function is called from. In the example above, foo’s return value is 32. Whenever we set x to whatever foo evaluates to—returns unto us, in other words—x will be set to that value. In this case, foo always evaluates to 32.

Let’s run this:

Foo returned 32.

That sure wasn’t too much of a big surprise—now was it?

Returning functions with arguments

You may also have returning functions make use of arguments:

fn main()
{
    println!("Foo returned {}.", foo(32));
    println!("Foo returned {}.", foo(12));
    println!("Foo returned {}.", foo(64));
}

fn foo(x: i32) -> i32
{
    x
}

This function returns whatever is inputted into it. To be fair, this is quite an uttely useless function—but imagine what you can do with it should you provide a higher number of arguments and start doing mathematical operations on the procided arguments in order to return something wonderful and fantastic!

Just look at this utterly useless output into the console:

Foo returned 32.
Foo returned 12.
Foo returned 64.

Returning early

Perhaps at some point you’d like to return earlier than the last line of a function. For this, we can make use of Rust’s return keyword:

fn main()
{
    println!("Foo returned {}.", foo(32));
    println!("Foo returned {}.", foo(12));
    println!("Foo returned {}.", foo(64));
}

fn foo(x: i32) -> i32
{
    return 2;

    x
}

Although this function just got a whole lot more meaningless, this explicitly returns the value of two prior to getting to the line where x is returned. Whenever a return occurs, the function is exited, since it already has managed to retrieve a value for the programmer.

This will print:

Foo returned 2.
Foo returned 2.
Foo returned 2.

How quaint.

Exercises

  • Make a function that takes a number as an argument and prints it out—finally, returning the exact same number. Set a let binding equal to the function upon calling. Call the same function using the let binding you managed to retrieve from the previous call.
  • Write a function that takes no argments, yet always returns the value of one.
  • Create a function that prints some interesting messages.

Moreover

If you’ve studied at any highschool—you most certainly know what a function is in mathematics. Functions in programming emerged from functions in mathematics. Depending on the programming language, functions can do slightly different things. For instance there’s Haskell, really proving how mathematical a programming language can be and still have a useful programming language—although Haskell isn’t really suited for the wonders we’re going to accomplish using Rust. On the other side, Haskell is extremely beautiful!