Back to the basics - processing input
In the previous chapter, we've looked at functions. In this chapter, we'll add a function to read some input from the user of our program, and do some basic processing on it.
fn main() { println!("What is your name?"); let input = read_string(); println!("Your name is: {input}"); } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); input }
This example prints the line: What is your name?
, waits for the user to type a name and hit the [Enter] button.
Then prints Your name is:
followed by the user's name.
We're using the read_string()
function to read the user's input. Let's explore that function.
We see that that read_string
returns a String
datatype.
A String
is a convenient wrapper around the str
data type, we've used in the previous chapters. Unlike str
it can be mutated which is exactly what we need in
our function.
Before we can read anything, we need to create a place to store the user's input. The String::new()
function creates
a new String
in memory, i.e., the 'drawer' that can hold a String
type. We use the mut
keyword to indicate that
we'll be changing the String
later on. The ::
separator is used to indicate that we'll be calling a function that
belongs to the preceding item. In this case we call the new()
function that belongs to the String
datatype.
A function that belongs to a datatype is called an associated function. Because the
new()
function is associated withString
, Rust knows we are creating a newString
.
The next line actually reads a line of text from the "standard input". stdin
is short for "standard input". The
standard input is usually the keyboard.
We're using Rust's standard library std::io
to take care of the actual reading. io
stands for input & output.
The dot .
chains together a sequence of operations. In our case:
std::io::stdin()
to get access to the keyboardread_line(&mut input)
to read a string of text and store the result ininput
expect("can not read user input")
to write an error in case something prevents us from reading (no keyboard??)
It does not matter if you write all these operations on a single line, or separate them over several lines. The
;
at the end determines the end of the statement, not the line break.
Notice that the read_line
function takes &mut input
as the input parameter. You've learned that the &
is used
to lend an item to a function. Similar to let
vs let mut
, we can indicate whether our variable can be changed by
the function or not. In the cabinet analogy, whether the contents of the drawer may be replaced by the function or not.
The &mut
keyword indicates that the read_line
function can modify the contents of the input
variable, i.e. the
drawer labelled with input
.
Finally the contents of input
is returned at the end of the read_string
function. Notice that there is no ;
at the
end of the input
statement. Omitting the ;
at the end of input
tells Rust to return the content of that variable
as a result of the function. It is shorthand for return input;
Which would essentially do the same.
I can see how the above is quite a lot to take in. Let's take some of these new concepts and dig into them a bit. We'll revisit our example later.
Strings, associated functions and methods
In the above example, we've seen that we can create a new String
with String::new()
. You've learned that the ::
separator is used to call an associated function; a function that belongs to the String
type. There are more
associated functions available on the String
type. RustRover editor will actually show what functions you can use when
you
pause typing after the ::
separator. There is the from()
associated function to create a new String
filled with
a piece of text.
fn main() { let my_name = String::from("Marcel"); println!("Your name is: {my_name}"); }
After we've created a String
we can call certain functions that belong to the String
object, not the type. These
are called "methods". A methods is called with the dot '.' separator, rather than the '::' separator. Here's an example:
fn main() { let my_name = String::from("Marcel"); let my_name_in_lower_case = my_name.to_lowercase(); println!("Your name is: {my_name_in_lower_case}"); }
So what is the difference between an associated function and a method?
I'll try to explain it with the same cabinet analogy we've used thus far.
Think of an associated function as an instruction that comes with the cabinet and describes how to construct the drawer. The instruction is related (or associated) with the drawer, but only tells you how to build the drawer. It does not describe how to manipulate the item that is stored in the drawer.
A method is an instruction that belongs to the finished drawer. It tells Rust how to manipulate an item that is stored in that specific drawer.
In our case the to_lowercase()
method takes the item from the my_name
drawer and converts all the characters to
lower
case. We are storing the result of the to_lowercase()
operation in a new drawer called: my_name_in_lower_case
.
Typically associated functions are used to create a new instance of a particular type. Methods are used to manipulate the particular type after it has been created.
Let's look at another String
method:
fn main() { let bad_name = String::from("Marcil"); let fixed_name = bad_name.replace("i", "e"); println!("Your name is: {}", fixed_name); }
Exercise
Call a method on a newly created
String
that converts the text to upper case and print the result.
Clippy
Now that our code becomes a bit more complex, we're likely going to run into some coding mistakes, or bugs. Rust comes with a 'friend' that can help identify these mistakes and who often offers some suggestions on how they can be remediated. This 'friend' is called: Clippy.
You can ask Clippy to inspect your program by:
- Double-tap the 'ctrl' key on your keyboard to open the "run anything" popup window.
- Type
cargo clippy
and press the Enter key
Inspect the output window
If it looks like this, you're good:
/Users/marcel/.cargo/bin/cargo clippy --color=always
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Process finished with exit code 0
You can use the down arrow key (⬇️) on your keyboard after opening the "run anything" window, on subsequent runs to quickly select
cargo clippy
from the list of previous commands.
Do not ignore the warning given by Clippy!
These warnings often point to a programming mistake or a missed opportunity for optimization.
Processing input
Back to our original example:
fn main() { println!("What is your name?"); let input = read_string(); println!("Your name is: {}", input); } fn read_string() -> String { let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("can not read user input"); input }
There is a method on String
that strips any empty characters and new-line breaks from a piece of text. It is called
trim()
.
Exercise
Modify the above example to clean the input with the
trim()
method and print the result. Try the example by adding a number of spaces to the beginning of your name when typing. Remember that you can run the program with the play (▶️) symbol in front offn main()
.
Exercise
Now modify your code to include a new function:
read_clean_string
, that reads the input, cleans it withtrim()
, and then returns it. You need to call theto_string()
method on the cleaned result to convert it to aString
. You can check the spoiler if you get stuck.
Congratulations! By completing the above exercise, you've created a fully functional program that reads some data, does some manipulation on the data and outputs it (to the screen). You will see that pretty much all software programs follow this exact paradigm.
In the next chapters we'll build on this concept, so make sure you have good understanding on variables and functions, before moving on.