Week 2 Exercises: Hello world

This assignment was written by Ryan Eberhardt and Armin Namavari.

Congratulations on making it to week 2! You rock!

Purpose

This week’s exercises are designed to get you comfortable compiling/running Rust code and using basic Rust syntax. The best way to learn any language (human, computer, or otherwise) is through immersion, so you can consider this your study abroad in Rustland. Make sure to post about it on Insta, get some DΓΆner Kebap, and enjoy the nightlife. We hope that this exercise will let us hit the ground running next week when we discuss concepts that you may not have seen yet in the languages you’ve studied.

We’ll include some advice for the assignment here, but you may also have to look through the Rust docs, Stack Overflow, etc. as well. We (Ryan and Julio) are available to you via Slack to help with the assignment. You are also welcome to reach out to your fellow peers and work together (you must however code up your own solutions for this assignment).

Due date: Thursday, April 15, 12:30pm (Pacific time)

This assignment should take 1-3 hours to complete. Even though this assignment is only meant to acclimate you to the Rust language, we expect it may be challenging; Rust is a pretty quirky language, and it will take some work to navigate that unfamiliarity. If you hit the 2 hour mark and aren’t close to finishing, please let us know so that we can address anything that might be tripping you up.

Part 1: Getting oriented

The first part of this week’s exercises will be getting a simple “hello world” program to run!

We have gotten Rust tooling set up for you on myth, so if you would like to develop your code there (as you do in CS 110), you should be good to go. However, if you would like to run your code on your local machine (e.g. because you have a poor internet connection or are on the other side of the world), you will need to install the Rust toolchain.

Regardless of whether you decide to work on myth or on your personal computer, you should take a moment to get your development environment set up for Rust. Ryan wrote up a list of tips that may be helpful. We highly recommend using VSCode with the rust-analyzer plugin, as this will show extra information about how the compiler is interpreting your code. (When you first install this extension, it will probably pop up a notice that rust-analyzer isn’t installed. Click the button to install it.) If you plan to work on myth, you should also install the SSH plugin. If you plan on using a different editor, we recommend installing rust-analyzer for your editor and checking out our list of tooling tips (especially for vim).

Now, onto getting the starter code! In this class, we will be using Github to manage assignment submissions. Github is an awesome collaboration platform built on git, which is a version control software. (Version control software allows you to manage different versions of your code; you can save snapshots across different points in time, and if your code ends up breaking, you can go back to a previous snapshot. It’s better than saving code.c, code-working.c, code-working-copy.c, code-final.c, code-final-seriously.c, and code-final-i-actually-submitted.c, and then being confused as to what is what.) git and Github are standard tools in industry, and if you haven’t encountered them before, we think it would be valuable to get some light and friendly exposure to them.

You should have received an email invitation to join the cs110l/week2-SUNETID repository. (If you did not, please let us know.) Once you have accepted, you can “clone” (download) that repository to your computer:

git clone https://github.com/cs110l/week2-YOURSUNETID.git week2

Then, cd into week2/part-1-hello-world. This directory contains a Rust package. You can see the source code in the src/ directory; check out src/main.rs.

Let’s try to compile this code! To do this, run the following command:

cargo build

Cargo is kind of like make, but it does quite a bit more. If your project has dependencies, Cargo will automatically take care of downloading and configuring those dependencies. (It does what npm does in Javascript land, and what setup.py does in Python land.) Cargo can also run automated tests, generate documentation, benchmark your code, and more. We won’t talk about this for now, but you will see some of this come in handy later in the quarter.

When you run cargo build, Cargo compiles the executable and saves it at target/debug/hello-word. Try running that now:

πŸ“  ./target/debug/hello-world
Hello, world!

As a convenience, Cargo offers a run command that both compiles and runs the program in one shot. Try modifying src/main.rs to print something new, then run cargo run (without running cargo build). Cargo will notice that the file has changed, recompile your code, and run the binary for you.

πŸ“  cargo run
   Compiling hello-world v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.77s
     Running `target/debug/hello-world`
You rock!

Congrats! You’ve run your first Rust program!

Part 2: Rust warmup (Shopping List)

Let’s write a super basic program in Rust! This program will read several strings from the user, then will print them back out to the screen. This isn’t terribly exciting functionality, but it will help us get familiar with Rust syntax. Much of this may feel familiar, but Rust has a few quirks when compared to other languages.

Here’s how the program should work when we are finished:

πŸ“  cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/part-2-shopping-list`
Enter an item to add to the list: apples
Enter an item to add to the list: bananas
Enter an item to add to the list: cucumbers
Enter an item to add to the list: done
Remember to buy:
* apples
* bananas
* cucumbers

Open part-2-shopping-list/src/main.rs in your text editor. We’ll be working through this file together.

Note: the Rust syntax overview was inspired by Will Crichton’s CS 242 Rust lab handout.

Numbers and variables

Numeric types in Rust include i8, i16, i32, and i64 (all of which store signed – positive or negative – numbers), as well as u8, u16, u32, and u64 (which store unsigned – strictly nonnegative – numbers). The numbers 8, 16, and so on indicate how many bits make up a value. This is intended to avoid problems that C encountered because the C specification did not define standard widths for numeric types. This is something you will encounter in assignment 2 in CS 110; today, on most computers, an int stores 4 bytes (corresponding to Rust’s i32), but you will be working with code from the 70s where an int only stored two bytes.

To declare a variable, we use the let keyword and specify the variable’s type:

// Declare a variable containing a signed 32-bit number. (Equivalent to
// "int n = 1" in C)
let n: i32 = 1;

Rust has a nifty feature called “type inference.” Instead of forcing you to declare the type of every variable, Rust allows you to omit the variable’s type when the compiler is able to figure out what the type should be. Most Rust code looks like this, and only includes explicit type annotations when the compiler can’t figure out which type should be used.

let n = 1;

Unlike most languages, variables in Rust are constants by default. This is intended to reduce errors; if you modify a variable that you didn’t intend to (i.e. didn’t explicitly mark as mutable), the compiler will give you an error. Add mut to make a variable mutable.

let mut n = 0;
n = n + 1;  // compiles fine

Strings

Strings are a little weird in Rust. There are two string types: &str and String. &str is an immutable pointer to a string somewhere in memory. For example:

let s: &str = "Hello world";    // the ": &str" annotation is optional

Here, the string Hello world is being placed in the program’s read-only data segment (that’s also how it works in C), and s is a read-only pointer to that memory.

The String type stores a heap-allocated string. You’re allowed to mutate Strings (so long as you use the mut keyword).

let mut s: String = String::from("Hello "); // "String" type annotation is optional
s.push_str("world!");

The first line allocates memory on the heap; s is a String object containing a pointer to that memory. The second line appends to the heap buffer, reallocating/resizing the buffer if necessary. The memory for the string will be automatically freed after the last line where s is used. We’ll talk more about the memory allocation/deallocation on Thursday’s lecture. (This is sort of similar to how C++ string works.)

Let’s code together: Reading input

Let’s implement an extra simple version of this shopping list program that only stores a single grocery item.

First, we need to get input from the user:

let input = prompt_user_input("Enter an item to add to the list: ");

println!("Remember to buy:");
println!("* {}", input);

When using print statements, {} serves as the placeholder for the thing you wish to print (like %d/%s/etc in C).

Note that even though we don’t explicitly specify the type of input, the compiler will see that prompt_user_input returns a String and will assign String as the type of input. (If you’re using VSCode with the rust-analyzer plugin, you should see the : String type annotation displayed.)

Collections

The easiest way to store collections of things is in a vector:

let mut v: Vec<i32> = Vec::new();
v.push(2);
v.push(3);
// Even here, the ": Vec<i32>" type annotation is optional. If you omit it, the
// compiler will look ahead to see how the vector is being used, and from that, it
// can infer the type of the vector elements. Neat!

Rust also supports fixed-size arrays. Unlike C, the length of the array is stored as part of the array type. In addition, array accesses are generally bounds-checked at runtime. This means that if there is an out-of-bounds access, the program will crash rather than allowing the access (as is the case in C/C++). No buffer overflows, please!

let mut arr: [i32; 4] = [0, 2, 4, 8];
arr[0] = -2;
println!("{}", arr[0] + arr[1]);

Let’s code together: Storing multiple inputs

Instead of prompting the user for only one input, let’s prompt for (and store) three shopping items.

First, we’ll make a vector to store the inputs:

let mut shopping_list = Vec::new();

Note that this needs to be declared mut so that we can modify it later. Also, even though there is nothing here that specifies that this is a vector of strings, the compiler will later notice that we put strings into the vector and deduce that Vec<String> is the appropriate type. Pretty cool!

Then, we can put several inputs into the list:

shopping_list.push(prompt_user_input("Enter an item to add to the list: "));
shopping_list.push(prompt_user_input("Enter an item to add to the list: "));
shopping_list.push(prompt_user_input("Enter an item to add to the list: "));

And then print those inputs:

println!("Remember to buy:");
println!("* {}", shopping_list[0]);
println!("* {}", shopping_list[1]);
println!("* {}", shopping_list[2]);

Loops

You can iterate over collections using iterators and some very nice, Python-like syntax:

for i in v.iter() { // v is the vector from above
    println!("{}", i);
}

// Alternate syntax, which compiles to the same thing as above:
for i in &v {
    println!("{}", i);
}

// Note that the & above is important! This code will work, but the loop
// will take ownership of each element in the vector, so you won't be able to
// use the vector afterwards:
for i in v { // no ampersand
    println!("{}", i);
}

While loops:

while i < 20 {
    i += 1;
}

Rust also has a special loop that should be used instead of while true:

let mut i = 0;
loop {
    i += 1;
    if i == 10 { break; }
}

If you’re curious why this loop exists, there is some discussion here. Long story short, it helps the compiler to make some assumptions about variable initialization.

Conditionals work pretty similar to languages you’re used to:

let i = 0;
if i < 10 {
    println!("i is less than 10!");
}

Let’s code together: Reading an arbitrary number of inputs

Let’s read inputs in a loop, breaking once the user enters “done”.

We can use a loop (“while true”) for this:

loop {
    let input = prompt_user_input("Enter an item to add to the list: ");
    if input.to_lowercase() == "done" {
        break;
    }
    shopping_list.push(input);
}

To print out the shopping list, we can use a for loop:

println!("Remember to buy:");
// This borrows an iterator from the list, and then we use the iterator to
// go through each element:
for item in &shopping_list {
    println!("* {}", item);
}

Functions

Finally, Rust functions are declared like so:

// Function with return type i32
fn sum(a: i32, b: i32) -> i32 {
    a + b
}

// Void function (no "->")
fn main() {
    // do stuff...
}

There are two potentially surprising things here:

Let’s code together: Decomposing into functions

We can easily break this program into two functions: one that reads input to form the shopping list, and one that prints the shopping list.

There are several ways you might write these functions. Try writing them yourself if you’re up for it! Here’s how I decided to write them:

fn read_shopping_list() -> Vec<String> {
    // Read the shopping list and return it

    // Remember: if you're trying to return a variable called "shopping_list"
    // at the end, the last line of this function should be "shopping_list"
    // instead of "return shopping_list;". Both would do the same thing, but
    // it's considered more idiomatic style to write the former.
}

fn print_shopping_list(shopping_list: &Vec<String>) {
    // Print shopping_list
}

fn main() {
    let shopping_list = read_shopping_list();
    print_shopping_list(&shopping_list);
}

Note that read_shopping_list is creating a vector and then passing ownership of that vector to main, which is then responsible for freeing any memory that was allocated. Then, main is passing a reference to print_shopping_list, so that print_shopping_list can look at the list but main still retains ownership.

In this case, since main doesn’t do anything after printing the shopping list, we could have transferred ownership to print_shopping_list instead of passing a reference. However, you might imagine modifying this program in the future so that main does more stuff (e.g. sync the shopping list to the cloud or some other fancy business), so main might want to retain ownership.

Part 3: Ownership short-answer exercises

Let’s work through some short-answer questions to get you thinking about ownership. For each of the following examples answer: Does this code compile? Explain why or why not. If it doesn’t compile, what could you change to make it compile? (If you’re not sure, you can run the compiler and try things out, but you need to provide a high-level English explanation of why it does/doesn’t work.)

Please provide your answers in part-3-ownership.txt.

Part 4: Guess the Word

Your goal is to implement a command-line guess-the-word game. The following is an example of a possible run of the game:

Welcome to Guess the Word!
The word so far is -------
You have guessed the following letters:
You have 5 guesses left
Please guess a letter: r

The word so far is ------r
You have guessed the following letters: r
You have 5 guesses left
Please guess a letter: s

The word so far is ---s--r
You have guessed the following letters: rs
You have 5 guesses left
Please guess a letter: t

The word so far is ---st-r
You have guessed the following letters: rst
You have 5 guesses left
Please guess a letter: l

The word so far is l--st-r
You have guessed the following letters: rstl
You have 5 guesses left
Please guess a letter: a
Sorry, that letter is not in the word

The word so far is l--st-r
You have guessed the following letters: rstla
You have 4 guesses left
Please guess a letter: b

The word so far is l-bst-r
You have guessed the following letters: rstlab
You have 4 guesses left
Please guess a letter: c
Sorry, that letter is not in the word

The word so far is l-bst-r
You have guessed the following letters: rstlabc
You have 3 guesses left
Please guess a letter: o

The word so far is lobst-r
You have guessed the following letters: rstlabco
You have 3 guesses left
Please guess a letter: e

Congratulations you guessed the secret word: lobster!

Alternatively, you may not have gotten the word:

Welcome to Guess the Word!
The word so far is --------
You have guessed the following letters:
You have 5 guesses left
Please guess a letter: a

The word so far is --a-----
You have guessed the following letters: a
You have 5 guesses left
Please guess a letter: b
Sorry, that letter is not in the word

The word so far is --a-----
You have guessed the following letters: ab
You have 4 guesses left
Please guess a letter: c

The word so far is c-a-----
You have guessed the following letters: abc
You have 4 guesses left
Please guess a letter: d
Sorry, that letter is not in the word

The word so far is c-a-----
You have guessed the following letters: abcd
You have 3 guesses left
Please guess a letter: e
Sorry, that letter is not in the word

The word so far is c-a-----
You have guessed the following letters: abcde
You have 2 guesses left
Please guess a letter: f

The word so far is c-a-f---
You have guessed the following letters: abcdef
You have 2 guesses left
Please guess a letter: g
Sorry, that letter is not in the word

The word so far is c-a-f---
You have guessed the following letters: abcdefg
You have 1 guesses left
Please guess a letter: h

The word so far is c-a-f--h
You have guessed the following letters: abcdefgh
You have 1 guesses left
Please guess a letter: i

The word so far is c-a-fi-h
You have guessed the following letters: abcdefghi
You have 1 guesses left
Please guess a letter: j
Sorry, that letter is not in the word

Sorry, you ran out of guesses!

The program exits either when you correctly complete the word or when you run out of guesses.

Advice and Helpful Hints

Optional: Linting your code

Rust has a style linter called Clippy that can check your code for a variety of errors. You can run it through cargo:

πŸ“  cargo clippy
    Finished dev [unoptimized + debuginfo] target(s) in 0.11s

Clippy is very helpful for improving your code style, as it will often provide suggestions for writing more “idiomatic” Rust. We aren’t grading style in this class, but you can run this for your own learning.

Part 5: Weekly survey

As you know, this is an experimental class, and we want to make it as enjoyable and meaningful of an experience as possible. Please let us know how you’re doing using this survey.

When you have submitted the survey, you should see a password. Put this code in part-5-survey.txt before submitting.

Submitting your work

As you work, you may want to commit your code to save a snapshot of your work. That way, if anything goes wrong, you can always revert to a previous state.

If you have never used git before, you may need to configure it by running these commands:

git config --global user.name "Firstname Lastname"
git config --global user.email "[email protected]"

Once you’ve done this, you won’t need to do it again.

Then, to commit your work, run this command:

git commit -am "Type some title here to identify this snapshot!"

In order to submit your work, commit it, then run git push. This will upload your commits (snapshots) to Github, where we can access them. You can verify that your code is submitted by visiting https://github.com/cs110l/week2-yourSunetid and browsing the code there. You can git push as many times as you’d like.

Grading

There are five components to this assignment, each worth 20%. You’ll earn the full 20% for each part if we can see that you’ve made a good-faith effort to complete it.