A bit more detailed explanation on functions

A function has always two parts, the declaration, and the call.

The declaration is this part with the fn keyword:


#![allow(unused)]
fn main() {
fn one() -> i64 {
    return 1;
}
}

Where the call is this code:


#![allow(unused)]
fn main() {
add_one();
}

You can have functions declared that are not called (you’ll get a warning), but you cannot have a function called that is not declared.

Also, we can call the same function as many times we want. It is the whole point of functions to be able to be called thousands of times.

To execute/call a function, we write the name and follow it by parenthesis:


#![allow(unused)]
fn main() {
add_one();
}

If we forget the parenthesis, the function will not be called:


#![allow(unused)]
fn main() {
add_one;  // WRONG!
}

The return value of a function can be used as a regular value:


#![allow(unused)]
fn main() {
let two = 1 + one();
let three = 1 + one() + 1;
}

Rust will execute the code in the function, get the return value and β€œreplace” the function call with that value:


#![allow(unused)]
fn main() {
let two = 1 + (1);
let three = 1 + (1) + 1;
}

Function names are actually variables. We won’t be using this β€œfeature” in this book, but it’s good that you know this is possible:


#![allow(unused)]
fn main() {
let one_fn = one;

let two = one_fn() + 1;
}

These are called function pointers. This is why it’s important you don’t forget the parenthesis, as the error might get you confused. Rust will understand that you want to store the function code somewhere1.


1

Actually they are function pointers, they point to the memory address where the code sits. But as usual, this goes outside what I want to explain.

They’re black boxes

We need to understand functions as a black box. Something goes in, something goes out. We don’t care that much on what happens inside when we call it.

You can imagine them as a microchip, that has pins for getting data in, and pins that give data out:

              β”Œβ€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β”
 number_1  -->β”‚                  β”‚
              β”‚                  β”‚
              β”‚  add_numbers()   β”‚---> result_number
              β”‚                  β”‚
 number_2  -->β”‚                  β”‚
              β””β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β”˜
              

This equates to this function:


#![allow(unused)]
fn main() {
fn add_numbers(number_1:i64, number_2:i64) -> i64 {
//     β”‚          β”‚             β”‚              ↳ Return value type
//     β”‚          β”‚             ↳ Second argument name and type
//     β”‚          ↳ First argument name and type
//     ↳ Function name
}

Remember: You can have functions that don’t return a value. A function that accepts one, two, or many arguments. Or both.

This function doesn’t need any arguments, and doesn’t return anything:


#![allow(unused)]
fn main() {
fn help() {
}

This one accepts one argument but doesn’t return anything:


#![allow(unused)]
fn main() {
fn manual(page: i64) {
}

This one doesn’t have arguments, yet has a return value:


#![allow(unused)]
fn main() {
fn calculate_pi() -> f64 {
}

Omitting β€œreturn”

In Rust, most of the time we don’t need to add return to return a value. If the last statement of the function lacks a semicolon, implies return.

For example:


#![allow(unused)]
fn main() {
fn add_numbers(a: i64, b: i64) -> i64 {
    let c = a + b;
    return c;
}
}

Can be simplified as:


#![allow(unused)]
fn main() {
fn add_numbers(a: i64, b: i64) -> i64 {
    let c = a + b;
    c
}
}

Which in turn, can be simplified again:


#![allow(unused)]
fn main() {
fn add_numbers(a: i64, b: i64) -> i64 {
    a + b
}
}

Nice!

Types as matching shapes

Remember those kids toys where you have to put the rectangle on the rectangular hole, and the circle in the circular hole?

Data types are somewhat like this.

Imagine that the types were shapes:

  • Integers are squares: β– 
  • floating points are circles: β—‰
  • Strings are parallelograms: β–°

Now, when you make a function, the arguments are like β€œsockets” you must connect:

              β”Œβ€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β”
 number_1 β—‹-->β”‚                  β”‚
              β”‚                  β”‚
              β”‚  add_numbers()   β”‚β—‰---> result_number
              β”‚                  β”‚
 number_2 β—‹-->β”‚                  β”‚
              β””β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β”˜             

A function that accepts several types would look like this:

   β”Œβ€”β€”β€”β€”β€”β€”β€”β€”β”
--β–’|        |
   |        |
--β—Ž|        |
   |        |
--β–±|        |
   β””β€”β€”β€”β€”β€”β€”β€”β€”β”˜             

You need to provide the right shapes to that box to get it working.

Or in specific terms, you need to provide the exact types the function is asking for to be able to call it.

You cannot call a function that accepts a f64 with an i64.

In the same sense, if your code is expecting an i64 as a result, the function must return that:

fn calc_pi() -> f64 {
    // code here...
    return pi;
}

fn main() {
    let pi:i64 = calc_pi(); // Error! calc_pi returns f64 but the variable is i64.
}

Returning multiple values

We can return more than one values by using tuples. A tuple is basically several values packed together. We put in between parenthesis, and each value separated by comma.


#![allow(unused)]
fn main() {
fn default_position() -> (f64, f64) {
    return (0, 0);
}
}

To use this, we can just unpack the values upon receiving:

fn main() {
    let (x, y) = default_position();
}

If we need to specify types, we can do for each variable:

fn main() {
    let (x: f64, y: f64) = default_position();
}

But we can also receive the return value as a single tuple:


#![allow(unused)]
fn main() {
let z: (f64, f64) = default_position();
}

To use this tuple, we can access the data in order by doing z.0 or z.1:


#![allow(unused)]
fn main() {
let z: (f64, f64) = default_position();
let x = z.0;
let y = z.1;
}

There’s not much mystery to this. Tuples are good to have several values packed together. And they’re used mainly for this use case, to get multiple return values in a function.