Home Posts Post Search Tag Search

Hands on Rust - 02 - First Steps with Rust
Published on: 2026-05-18 Tags: Rust, Hands on Rust

2. First Steps with Rust

Okay now we get to go even further. We learned to install and create a very basic “Hello World!” project. We will create a new project that we will slowly add more and more functionality to.

Creating a New Project

Okay to go over the steps one more time:

1. Open a terminal/command prompt.
2. Change the directory to your “Learn Rust” directory. e.g., cd /home/bert/learnrust).
3. Use Cargo to make a new “treehouse” project: type cargo new treehouse.
4. Launch your favorite text editor/IDE, and load your new project.

Do that now and we can meet in the next section.

Capturing User Input

We will now create a program that will ask for some user input and then the program will print some results, then we can use formatting to print a personalized greeting.

Prompting for the Visitor’s Name

Okay so we first need to show the user some text. Open up the src/main.rs and replace the println! with this.

println!("Hello, what's your name?");

Storing the Name in a Variable

Rust naturally will make variables immutable but there is a mut keyword that will allow a programmer to change a variable once it is defined. Add this second line.

let mut your_name = String::new();

The let will allow you to describe a new variable. mut makes the variable mutable, meaning its value can be changed. = is an assignment. String::new(); is the value to assign to the variable.

Receiving Keyboard Input

Rust has some terminal input and functions like, std::io::stdin or std::io::stdin::read_line, but these are a lot of typing for a simple input. There is a way to use the use keyword so you don’t have to type it out every time. Add the following to the top of the same file.

use std::io::stdin;

Reading User Input

Now that we have access to the stdin we can use that to store the user’s input. Add this line after the variable declaration.

    stdin()
        .read_line(&mut your_name)
        .expect("Failed to read line");
}

We can see a few things from this function. First the is function chaining anytime that you have a string of functions that have an input and a return value you can pass the return value right into the next function. Here are the other 2 things that you can learn from the above code:

• Prefixing a variable with an ampersand (&) creates a reference to the
variable. Also the &mut allows you to change the value.
• You expect the read_line function to work correctly. If it doesn’t, your program
will crash. Rust is returning a Result object, and you are checking that the
function worked by calling expect.

Printing with Placeholders

Okay so now we have the persons name in the variable we now need to print the value into a string. Add this line to below the next set of functions.

println!("Hello, {}", your_name)

Use cargo run to start the program.

Moving Input to a Function

Now we get to move on to a basic version of abstraction where we take an advanced topic and boil it down to a single function. We will be asking for a users name a lot in this chapter so we want to make a function what_is_your_name(). Let’s create that now and then update our main to use it.

use std::io::stdin;
fn what_is_your_name() -> String {
    let mut your_name = String::new();
    stdin()
        .read_line(&mut your_name)
        .expect("Failed to read line");
    your_name
}
fn main() {
    println!("Hello, what's your name?");
    let name = what_is_your_name();
    println!("Hello, {}", name);
}

So we have a new function that will do what we had before. A couple things to keep in mind:

The last thing within a function will be the return value
When you define a function using the -> String will denote that it is returning a string.
When you don't have a ; at the end it is to denote a return value.
We will store the value of the function in a new variable.

Trimming Input

We want to make sure that we only print what we want and during the right conditions out output will be more than we want. There is a way to see more about the output with the . This is the debug placeholder. Let’s replace {} with the debug placeholder.

println!("{:?}", name);

# This will print

Hello, what's your name?
jimmy
Hello, "jimmy\n"

As you can see we have some other characters within the print statement. We can now start to leverage some of the built-in functions of Rust to: make the input all lowercase, and even trim some of the extra values. Let’s update the last time to do those things too.

fn what_is_your_name() -> String {
    let mut your_name = String::new();
    stdin()
        .read_line(&mut your_name)
        .expect("Failed to read line");
    your_name.trim().to_lowercase()
}

Now that is much better.

Storing Strings in an Array

We want to have an exclusive club that will only allow certain people in the club. We can start with a test to see if your name is “bert” with if/else statements. We wont be adding all this to the code but we can show you some basic versions.

if your_name == "bert" {
    println!("Welcome.");
} else {
    println!("Sorry, you are not on the list.")
}

// Let's add steve to the list too

if your_name == "bert" || your_name == "steve" {
  println!("Welcome.");
} else {
    println!("Sorry, you are not on the list.")
}

Now you can see that we can have many people in the list but it will get unwieldy very quick let’s talk now about arrays.

Declaring an Array

Before when we simply said let name = what_is_your_name() rust inferred the type of variable. Here we can do the same with the declaration of an array.

let visitor_list = ["bert", "steve", "fred"];

This is an array that we can access values with the array[index] syntax. The official way to determine and set a string would be let visitor_list:[&str;3] =. Lastly we should talk about the fact that there are 2 types of strings. The str is a literal and are generally immutable, there is an other and that is String. This latter one stores, location, length, capacity and you can append Strings and edit them.

Searching an Array

For a min let’s talk about for loops in Rust. We have a few different ways of going through an array. Ill try and put a few below some will not be very Rust like or even the best way of doing things but we can start small and build from there.

for i in 0..10 {
  println!("{}", i)
}

// We can then use the list length
for i in 0..list.len() {
  // code here
}

// here is a better more Rust like approach
for visitor in &visitor_list {
  // Code here
}

So we want to be able to set a variable that will be set to true if a person’s name is found within a list of allowed visitors. This might not work the best for testing against more than 1 person but right now we just are checking against a single name.

let visitor_list = ["bert", "steve", "fred"];
let mut allow_them_in = false;
for visitor in &visitor_list {
    if visitor == &name {
        allow_them_in = true;
    }
}
if allow_them_in {
    println!("Welcome to the Treehouse, {}", name);
} else {
    println!("Sorry, you aren't on the list.");
}

Here is the output of the program

➾ cargo run
Hello, what's your name?
Bert
Welcome to the Treehouse, Bert

➾ cargo run
Hello, what's your name?
bob
Sorry, you aren't on the list.

Grouping Data with Structs

Let’s move onto struct that contain member fields. This is so very similar to the Elixir Struct. Let’s add this to the top of the code.

struct Visitor {
    name: String,
    greeting: String,
}

// Now we can set up the constructor and the methods we want for it
impl Visitor {
    fn new(name: &str, greeting: &str) -> Self {
        Self {
            name: name.to_lowercase(),
            greeting: greeting.to_string(),
        }
    }
    fn greet_visitor(&self) {
        println!("{}", self.greeting);
    }
}

So let’s go over a few things:

impl will set up the implement
new is the associated function. You can call this with Visitor::new() it returns a Self and and has the parameters that we need
Selfs refers to the struct type and self refers to the instance of the structure.
greet_visitor(&self) is a member function and takes self as a parameter. 

Now we can set the visitor list in a far better fashion.

    let visitor_list = [
        Visitor::new("bert", "Hello Bert, enjoy your treehouse."),
        Visitor::new("steve", "Hi Steve. Your milk is in the fridge."),
        Visitor::new("fred", "Wow, who invited Fred?"),
    ];

Searching with Iterators

iterators are Rust way of manipulating data. We can replace the for loop with the following, we will go over the code after.

    let known_visitor = visitor_list
        .iter()
        .find(|visitor| visitor.name == name);

❶ Assign the result of the iterator function chain to the variable known_visitor.
❷ Create an iterator with iter() that contains all of the data from the visitor_list.
❸ find() runs a closure. If the closure returns true, find() returns the matching
value. The semicolon finishes the statement.

Closures will also capture the data that runs them so you where able to use visitor.name


Remember that the known_visitor is going to contain a Visitor if they are in the list of visitors or nothing if not. Now we can leverage the match to give 2 options for what to do in either case.

    match known_visitor {
        Some(visitor) => visitor.greet_visitor(),
        None => println!("You are not on the visitor list. Please leave."),
    }

We are saying that if we find a visitor we will return the visitor.greeting. If not the print. This is similar to a case and pattern matching in Elixir. Let’s test the new code.

Hello, what's your name?
Bert
Hello Bert, enjoy your treehouse.

Hello, what's your name?
Steve
Hi Steve. Your milk is in the fridge.

Hello, what's your name?
John
You are not on the visitor list. Please leave.

Storing a Variable Amount of Data with Vectors

Vectors are like arrays, but where arrays will have a set size Vectors can be added to with push().

Deriving Debug

We can’t just print out a Struct like we would with a String. In this case we want to add in the debug placeholders for the Visitor struct.

#[derive(Debug)]
struct Visitor {
    name: String,
    greeting: String,
}

Now that we have the derived Debug (so long as every member field can support it), you can use the place holder to print the entire struct.

Using a Vector Instead of an Array

Vec is a way to build arrays that can grow with the data that you want in it. vec! is a built-in syntax to allow for not having to use the vector_list.push() for each bit of data going into the vector.

    let mut visitor_list = vec![
        Visitor::new("Bert", "Hello Bert, enjoy your treehouse."),
        Visitor::new("Steve", "Hi Steve. Your milk is in the fridge."),
        Visitor::new("Fred", "Wow, who invited Fred?"),
    ];

// This is also equivalent to
    let mut visitor_list = Vec::new();
    visitor_list.push(
      Visitor::new("bert", "Welcome Text.")
    );
// Then add all the other visitors.

Looping Until You Call Break

loop is a way to keep running an action until you run break. In this case we want to be able to add members until we are satisfied with the list then print the members. Let’s quickly go over the syntax for loop then add the functionality we want for our Treehouse.

loop {
  println!("Hello, what's your name? (Leave empty and press ENTER to quit)");
  ...
  break; // Execution continues after the loop
}
// Execution resumes here on break

Adding New Vector Entries

After a visitor registers, you need to:
1. See if the user entered a blank name, and if so, break out of the loop.
2. If they’re a new visitor, use push to add them to the visitor list.

There is quite a few changes that need to be made so I’ll give the entire code here.

fn main() {
    let mut visitor_list = vec![
        Visitor::new("Bert", "Hello Bert, enjoy your treehouse."),
        Visitor::new("Steve", "Hi Steve. Your milk is in the fridge."),
        Visitor::new("Fred", "Wow, who invited Fred?"),
    ];

    loop {
        println!("Hello, what's your name? (Leave empty and press ENTER to quit)");

        let name = what_is_your_name();
        let known_visitor = visitor_list.iter().find(|visitor| visitor.name == name);

        match known_visitor {
            Some(visitor) => visitor.greet_visitor(),
            None => {
                if name.is_empty() {
                    break;
                } else {
                    println!("{} is not on the visitor list.", name);
                    visitor_list.push(Visitor::new(&name, "New friend"));
                }
            }
        }
    }

    println!("The final list of visitors:");
    println!("{:#?}", visitor_list);
}

/// This next bit is the output
Hello, what's your name? (Leave empty and press ENTER to quit)
jimmy
jimmy is not on the visitor list.
Hello, what's your name? (Leave empty and press ENTER to quit)

The final list of visitors:
[
    Visitor {
        name: "bert",
        greeting: "Hello Bert, enjoy your treehouse.",
    },
    Visitor {
        name: "steve",
        greeting: "Hi Steve. Your milk is in the fridge.",
    },
    Visitor {
        name: "fred",
        greeting: "Wow, who invited Fred?",
    },
    Visitor {
        name: "jimmy",
        greeting: "New friend",
    },
]

So we have the ability to add in friends and then print out the entire list once we are satisfied.

Categorizing with Enumerations

The final set of improvements to the visitor list are:

• Store an action associated with a visitor: admit them, admit them with a
note, refuse entry, or mark them as probationary treehouse members.
• Store the visitor’s age, and forbid them from drinking if they’re under 21.

We can do this with Enumerations.

Enumerations

Towards the top of the file above the functions we can set an enumerations. Enumerations are designed to give set value for a given set of data. Let’s define the Enum then we can go over it.

#[derive(Debug)]
enum VisitorAction {
    Accept,
    AcceptWithNote { note: String },
    Refuse,
    Probation,
}

We have the 4 actions. We still see the #[derive(Debug)] that allows for a better print in this case. The declaration of enum VisitorAction {. Action id a enumeration option can be used with let visitor_action = VisitorAction::Accept;. Then the AcceptWithNote can be used with visitor_action = VisitorAction::AcceptWithNote{ note: “my note”.to_string() };

Using Enumerations and Integer Data Members

Now let’s add an extra field to the Visitor so we can keep track of the action.

#[derive(Debug)]
struct Visitor {
    name: String,
    action: VisitorAction,
    age: i8,
}

impl Visitor {
    fn new(name: &str, action: VisitorAction, age: i8) -> Self {
        Self {
            name: name.to_lowercase(),
            action,
            age,
        }
    }
}

Assigning Enumerations

Looking at the Enumerations we can access the members with the :: operator. So for the VisitorAction we could use VisitorAction::Accept. Let’s replace the list initialization with the following.

fn main() {
    let mut visitor_list = vec![
        Visitor::new("Bert", VisitorAction::Accept, 45),
        Visitor::new(
            "Steve",
            VisitorAction::AcceptWithNote {
                note: String::from("Lactose-free milk is in the fridge"),
            },
            15,
        ),
        Visitor::new("Fred", VisitorAction::Refuse, 30),
    ];

Matching Enumerations

This is where pattern matching will come in. It can check to be sure a condition is true and also extract fields. Let’s build a greet_visitor() function for the Visitor.

    fn greet_visitor(&self) {
        match &self.action {
            VisitorAction::Accept => println!("Welcome to the tree house, {}", self.name),
            VisitorAction::AcceptWithNote { note } => {
                println!("Welcome to the treehouse, {}", self.name);
                println!("{}", note);
                if self.age < 21 {
                    println!("Do not serve alcohol to {}", self.name);
                }
            }
            VisitorAction::Probation => println!("{} is now a probationary member", self.name),
            VisitorAction::Refuse => println!("Do not allow {} in!", self.name),
        }
    }

  /// Now we can change the push line to add new member with probation status
  visitor_list.push(Visitor::new(&name, VisitorAction::Probation, 0));
  

We now have the ability to assign a greeting based off the values within a Visitor. This is so close to the Elixir case. You can see what we are matching against at the match line, then the pattern matching for the different values of the Action. We even have a check against the age of the to see if they should be served alcohol. Let’s look at some outputs

Hello, what's your name? (Leave empty and press ENTER to quit)
bert
Welcome to the tree house, bert
Hello, what's your name? (Leave empty and press ENTER to quit)
steve
Welcome to the treehouse, steve
Lactose-free milk is in the fridge
Do not serve alcohol to steve
Hello, what's your name? (Leave empty and press ENTER to quit)
fred
Do not allow fred in!
Hello, what's your name? (Leave empty and press ENTER to quit)
joebob
joebob is not on the visitor list.
Hello, what's your name? (Leave empty and press ENTER to quit)

The final list of visitors:
[
    Visitor {
        name: "bert",
        action: Accept,
        age: 45,
    },
    Visitor {
        name: "steve",
        action: AcceptWithNote {
            note: "Lactose-free milk is in the fridge",
        },
        age: 15,
    },
    Visitor {
        name: "fred",
        action: Refuse,
        age: 30,
    },
    Visitor {
        name: "joebob",
        action: Probation,
        age: 0,
    },
]

Wrap-Up

You achieved a lot in this chapter and put to use many of Rust’s basic concepts, enough to write useful and fun programs. In particular, you learned:

• Printing and formatting text
• Working with strings
• Using for and loop for program flow
• Using if for conditional execution
• Arrays
• Structures
• Vectors
• Enumerations
• Match statements