Variables & functions

Our goal for this chapter is to develop confidence about basic functions and variables in Rust.

All of our project’s source files live in the src directory, and for the purposes of clarity, we’re only going to use the main.rs file for our markdown compiler.

The Hello World example code provided gives us all we need to know to start customizing our project. By the end of this chapter, executing our code without arguments will output a banner–a short block of text that includes the program’s name, the author, a brief description, and usage examples.

# Create a function in Rust

The first thing we are going to write in Rust is a function.

A function is defined with the fn keyword, like this:

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

Above our main() function, let’s create a new function called usage(). Inside it, we’ll use the same macro used to write "Hello, world!" to output the name and a short description of our tool:

fn usage() {
    println!("tinymd, a markdown compiler written by <YOUR NAME>");
}

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

Go ahead and replace <YOUR NAME> with your name if you haven’t already done so.

Compile and run your project with cargo run -q, which will always automatically detect changes in your source files and rebuild your project if needed:

$ cargo run -q
Hello, world!
What was -q for again?

The -q flag in cargo run -q invokes “quiet” mode.

Here’s without:

 $ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
      Running `target\debug\tinymd.exe`
  Hello, world!

Here’s with:

$ cargo run
Hello, world!
What if I see a `warning: function is never used` message?

If you run the code above, you’ll likely see a warning that looks like this:

warning: function is never used: `usage`
 --> src/main.rs:1:4
  |
1 | fn usage() {
  |    ^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

If it wasn’t already apparent, you can ignore this warning because it is just Rust telling us we have a function that we’ve declared but not used. This warning will go away once we actually call the function.

If you don’t really care to look at the accompanying information (Finished dev and Running lines), you can pass the -q flag to Cargo, which tells it to be quiet:

$ cargo run -q
Hello, world!

To save space, that’s how we’ll be compiling, building, and running the code as we make changes to it—so don’t panic when you see the -q flag.

Now that we have a function to print the banner, let’s replace the println! command in the main function with a call to usage():

fn usage() {
    println!("tinymd, a markdown compiler written by <YOUR NAME>");
}

fn main() {
    usage();
}

Finally, let’s compile, build, and run, all in one command:

$ cargo run -q
tinymd, a markdown compiler written by <YOUR NAME>

With our usage() function scaffolded, let’s iterate and have it return a simple value that we can write to the console window.

# Return a value from a function

In this section, we’re going to create a function called get_version() that will return some arbitrary version number of our tool.

We saw earlier that a function with no arguments and no return value is written like this:

fn get_version() {  
}

When we want a function to return a value, we append -> and the return value’s type to the declaration, like this:

fn get_version() -> u16 {  
}

Let’s say our version number is 1000, and we want to return that from a function and then print it out. Recall that the range of an unsigned integer is 0 to 2X-1, meaning we would need to store the number 1000 in at least a 16-bit unsigned integer (which has a range of 0 to 65,535). We denote that in Rust with the keyword u16.

To tell a function to return a u16, we write it like this:

fn get_version() -> u16 {
}

This declares a function named get_version that takes no arguments and returns a u16. You can see that we specify return values by using the -> set of symbols.

To return the value 1000, we can either use the return keyword (which does exactly what it sounds like) or we can do what Ruby does and just write the number.

The following functions do the same exact thing:

fn get_version() -> u16 {
  1000
}

fn get_version() -> u16 {
  return 1000;
}

In both examples above, the functions will evaluate to the number 1000. This is because Rust is considered an expression-based language (like Ruby!). In an expression-based language, everything is an expression–and expressions evaluate to values. That means that a block of code, being an expression, evaluates to a value. Since a function is a block of code, functions evaluate to a value, too.

Notice that we only need the semi-colon when we use the return keyword; the number 1000 by itself is the value that the block evaluates to, while the return 1000; is a statement–and statements end with a semicolon.

The accepted way in the Rust community (and in most expression-based languages) is to only use the return keyword for early returns–that is, for when the last statement in a particular block may not be the only value that the block can evaluate to. For example, if you wanted to print some output based on the version number, you would do something like this:

fn get_version() -> u16 {
    let version = 1; // For the sake of example
    
    if version < 2 {
        return 1;
    } 

    2
}

Obviously this is a pointless block of code, but you can see that the block will evaluate to 1, and in the satisifed if expression, we use the return keyword to indicate an early return. It’s an early return because the block would normally evaluate to 2, but in the case of the early if check, it could return a value earlier than when 2 would be returned.

Let’s add the new get_version() function to our program by calling it from within the println!() macro in usage():

fn get_version() -> u16 {
    1000
}

fn usage() {
    println!("tinymd, a markdown compiler written by <YOUR NAME>");
    println!("Version {}", get_version());
}

fn main() {
    usage();
}

Here you can see how we pass arguments to println!() like we would a typical printf function in another language. Since Rust provides this macro for us, it can discern what kind of variable you are trying to print so you don’t have to worry about specifying the type. For example, in C’s printf function you would use %s to denote where you want the character array to be printed; Rust only requires you to use {}, regardless of the variable’s type. (We will be printing strings very soon, so sit tight!)

Go ahead and compile, build, and run this with the following command:

$ cargo run -q
tinymd, a markdown compiler written by <YOUR NAME>
Version 1000

The version integer is being printed, and the println! macro can infer that it’s an integer based on the return type specified by get_version().

We’re almost done with this chapter. Now that we’re confident with Rust’s function syntax, let’s learn how to create and assign a value to a simple variable.

# Create an integer variable

The first kind of variable we will learn about is the integer. All variables in Rust are declared by putting their type after their name. For example, if we want to create an integer variable to hold the version of our application, we would declare a variable version like this:

let version: u16;  // I'm using u16 for the sake of example only. This could be
                   // a u8, too. It just depends on your variable size.

Recall that u16 is short for unsigned 16-bit integer.

Variables in Rust are declared with the let keyword, and then we use a colon (:) to describe the variable’s type. All variables need to be declared like this, unless the variables type can be inferred by, say, the return value of a function. Let’s practice using both ways to declare a variable.

To store our arbitrary application version in a variable, let’s declare it within the scope of the usage() function. Then, instead of having println! use the function get_version() to print the version, we’ll have it use our local variable:

fn get_version() -> u16 {
    1000
}

fn usage() {
    let the_version: u16;  // Declare our variable
    the_version = get_version();  // Assign a value to our variable
    println!("tinymd, a markdown compiler written by <YOUR NAME>");
    println!("Version {}", the_version);  // Print the value assigned
}

fn main() {
    usage();
}

As you can see, substituting the function with the variable in println! is fairly straightforward. Additionally, look at how we declared the_version, then passed the value of the function to it. Rust infers the type of variable we want based on the return value of the function we use to assign it a value.

We can improve this by letting Rust infer the_version’s type:

fn get_version() -> u16 {
    1000
}

fn usage() {
    let the_version = get_version();
    // ...
    println!("Version {}", the_version);
}

fn main() {
    usage();
}

Neat!

In this chapter, we learned about functions and integer variables. Next, we’ll start diving into how Rust thinks about memory as we create both dynamic and static strings to hold different kinds of information.

“Success is stumbling from failure to failure with no loss of enthusiasm.”

— Winston Churchill

📦 All the code up to this point is available on GitHub.

Checkpoint

Before moving on, you should be able to confidently:

  • Create a function without errors

  • Create an integer variable without errors

  • Print an integer variable to the command line without errors