Writing Hello World!
In this section, we are going to write a very basic program, Hello World!. After we successfully compile that, we are going to write a more complex program to see the basic capabilities of the Rust language. Let's do it by following these instructions:
- Let's create a new folder, for example,
01HelloWorld
. - Create a new file inside the folder and give it the name
main.rs
. - Let's write our first code in Rust:
fn main() { println!("Hello World!"); }
- After that, save your file, and in the same folder, open your terminal, and compile the code using the
rustc
command:rustc main.rs
- You can see there's a file inside the folder called
main
; run that file from your terminal:./main
- Congratulations! You just wrote your first
Hello World
program in the Rust language.
Next, we're going to step up our Rust language game; we will showcase basic Rust applications with control flow, modules, and other functionalities.
Writing a more complex program
Of course, after making the Hello World
program, we should try to write a more complex program to see what we can do with the language. We want to make a program that captures what the user inputted, encrypts it with the selected algorithm, and returns the output to the terminal:
- Let's make a new folder, for example,
02ComplexProgram
. After that, create themain.rs
file again and add themain
function again:fn main() {}
- Then, use the
std::io
module and write the part of the program to tell the user to input the string they want to encrypt:use std::io; fn main() { println!("Input the string you want to encrypt:"); let mut user_input = String::new(); io::stdin() .read_line(&mut user_input) .expect("Cannot read input"); println!("Your encrypted string: {}", user_input); }
Let's explore what we have written line by line:
- The first line,
use std::io;
, is telling our program that we are going to use thestd::io
module in our program.std
should be included by default on a program unless we specifically say not to use it. - The
let...
line is a variable declaration. When we define a variable in Rust, the variable is immutable by default, so we must add themut
keyword to make it mutable.user_input
is the variable name, and the right hand of this statement is initializing a new emptyString
instance. Notice how we initialize the variable directly. Rust allows the separation of declaration and initialization, but that form is not idiomatic, as a programmer might try to use an uninitialized variable and Rust disallows the use of uninitialized variables. As a result, the code will not compile. - The next piece of code, that is, the
stdin()
function, initializes thestd::io::Stdin
struct. It reads the input from the terminal and puts it in theuser_input
variable. Notice that the signature forread_line()
accepts&mut String
. We have to explicitly tell the compiler we are passing a mutable reference because of the Rust borrow checker, which we will discuss later in Chapter 9, Displaying User's Post. Theread_line()
output isstd::result::Result
, an enum with two variants,Ok(T)
andErr(E)
. One of theResult
methods isexpect()
, which returns a generic typeT
, or if it's anErr
variant, then it will cause panic with a generic errorE
combined with the passed message. - Two Rust enums (
std::result::Result
andstd::option::Option
) are very ubiquitous and important in the Rust language, so by default, we can use them in the program without specifyinguse
.
Next, we want to be able to encrypt the input, but right now, we don't know what encryption we want to use. The first thing we want to do is make a trait, a particular code in the Rust language that tells the compiler what functionality a type can have:
- There are two ways to create a module: create
module_name.rs
or create a folder withmodule_name
and add amod.rs
file inside that folder. Let's create a folder namedencryptor
and create a new file namedmod.rs
. Since we want to add a type and implementation later, let's use the second way. Let's write this inmod.rs
:pub trait Encryptable { fn encrypt(&self) -> String; }
- By default, a type or trait is private, but we want to use it in
main.rs
and implement the encryptor on a different file, so we should denote the trait as public by adding thepub
keyword. - That trait has one function,
encrypt()
, which has self-reference as a parameter and returnsString
. - Now, we should define this new module in
main.rs
. Put this line before thefn
main block:pub mod encryptor;
- Then, let's make a simple type that implements the
Encryptable
trait. Remember the Caesar cipher, where the cipher substitutes a letter with another letter? Let's implement the simplest one calledROT13
, where it converts'a'
to'n'
and'n'
to'a'
,'b'
to'o'
and'o'
to'b'
, and so on. Write the following in themod.rs
file:pub mod rot13;
- Let's make another file named
rot13.rs
inside theencryptor
folder. - We want to define a simple struct that only has one piece of data, a string, and tell the compiler that the struct is implementing the
Encryptable
trait. Put this code inside therot13.rs
file:pub struct Rot13(pub String); impl super::Encryptable for Rot13 {}
You might notice we put pub
in everything from the module declaration, to the trait declaration, struct declaration, and field declaration.
- Next, let's try compiling our program:
> rustc main.rs error[E0046]: not all trait items implemented, missing: `encrypt` --> encryptor/rot13.rs:3:1 | 3 | impl super::Encryptable for Rot13 {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `encrypt` in implementation | ::: encryptor/mod.rs:6:5 | 6 | fn encrypt(&self) -> String; | ---------------------------------------------- ------ `encrypt` from trait error: aborting due to previous error For more information about this error, try `rustc --explain E0046`.
What is going on here? Clearly, the compiler found an error in our code. One of Rust's strengths is helpful compiler messages. You can see the line where the error occurs, the reason why our code is wrong, and sometimes, it even suggests the fix for our code. We know that we have to implement the super::Encryptable
trait for the Rot13
type.
If you want to see more information, run the command shown in the preceding error, rustc --explain E0046
, and the compiler will show more information about that particular error.
- We now can continue implementing our
Rot13
encryption. First, let's put the signature from the trait into our implementation:impl super::Encryptable for Rot13 { fn encrypt(&self) -> String { } }
The strategy for this encryption is to iterate each character in the string and add 13 to the char value if it has a character before 'n'
or 'N'
, and remove 13 if it has 'n'
or 'N'
or characters after it. The Rust language handles Unicode strings by default, so the program should have a restriction to operate only on the Latin alphabet.
- On our first iteration, we want to allocate a new string, get the original
String
length, start from the zeroeth index, apply a transformation, push to a new string, and repeat until the end:fn encrypt(&self) -> String { let mut new_string = String::new(); let len = self.0.len(); for i in 0..len { if (self.0[i] >= 'a' && self.0[i] < 'n') || (self.0[i] >= 'A' && self.0[i] < 'N') { new_string.push((self.0[i] as u8 + 13) as char); } else if (self.0[i] >= 'n' && self.0[i] < 'z') || (self.0[i] >= 'N' && self.0[i] < 'Z') { new_string.push((self.0[i] as u8 - 13) as char); } else { new_string.push(self.0[i]); } } new_string }
- Let's try compiling that program. You will quickly find it is not working, with all errors being
`String` cannot be indexed by `usize`
. Remember that Rust handles Unicode by default? Indexing a string will create all sorts of complications, as Unicode characters have different sizes: some are 1 byte but others can be 2, 3, or 4 bytes. With regard to index, what exactly are we saying? Is index means the byte position in aString
, grapheme, or Unicode scalar values?
In the Rust language, we have primitive types such as u8
, char
, fn
, str
, and many more. In addition to those primitive types, Rust also defines a lot of modules in the standard library, such as string
, io
, os
, fmt
, and thread
. These modules contain many building blocks for programming. For example, the std::string::String
struct deals with String
. Important programming concepts such as comparison and iteration are also defined in these modules, for example, std::cmp::Eq
to compare an instance of a type with another instance. The Rust language also has std::iter::Iterator
to make a type iterable. Fortunately, for String
, we already have a method to do iteration.
- Let's modify our code a little bit:
fn encrypt(&self) -> String { let mut new_string = String::new(); for ch in self.0.chars() { if (ch >= 'a' && ch < 'n') || (ch >= 'A' && ch < 'N') { new_string.push((ch as u8 + 13) as char); } else if (ch >= 'n' && ch < 'z') || (ch >= 'N' && ch < 'Z') { new_string.push((ch as u8 - 13) as char); } else { new_string.push(ch); } } new_string }
- There are two ways of returning; the first one is using the
return
keyword such asreturn new_string;
, or we can write just the variable without a semicolon in the last line of a function. You will see that it's more common to use the second form. - The preceding code works just fine, but we can make it more idiomatic. First, let's process the iterator without the
for
loop. Let's remove the new string initialization and use themap()
method instead. Any type implementingstd::iter::Iterator
will have amap()
method that accepts a closure as the parameter and returnsstd::iter::Map
. We can then use thecollect()
method to collect the result of the closure into its ownString
:fn encrypt(&self) -> Result<String, Box<dyn Error>> { self.0 .chars() .map(|ch| { if (ch >= 'a' && ch < 'n') || (ch >= 'A' && ch < 'N') { (ch as u8 + 13) as char } else if (ch >= 'n' && ch < 'z') || ( ch >= 'N' && ch < 'Z') { (ch as u8 - 13) as char } else { ch } }) .collect() }
The map()
method accepts a closure in the form of |x|...
. We then use the captured individual items that we get from chars()
and process them.
If you look at the closure, you'll see we don't use the return
keyword either. If we don't put the semicolon in a branch and it's the last item, it will be considered as a return
value.
Using the if
block is good, but we can also make it more idiomatic. One of the Rust language's strengths is the powerful match
control flow.
- Let's change the code again:
fn encrypt(&self) -> String { self.0 .chars() .map(|ch| match ch { 'a'..='m' | 'A'..='M' => (ch as u8 + 13) as char, 'n'..='z' | 'N'..='Z' => (ch as u8 - 13) as char, _ => ch, }) .collect() }
That looks a lot cleaner. The pipe (|
) operator is a separator to match items in an arm. The Rust matcher is exhaustive, which means that the compiler will check whether all possible values of the matcher are included in the matcher or not. In this case, it means all characters in Unicode. Try removing the last arm and compiling it to see what happens if you don't include an item in a collection.
You can define a range by using ..
or ..=
. The former means we are excluding the last element, and the latter means we are including the last element.
- Now that we have implemented our simple encryptor, let's use it in our main application:
fn main() { ... io::stdin() .read_line(&mut user_input) .expect("Cannot read input"); println!( "Your encrypted string: {}", encryptor::rot13::Rot13(user_input).encrypt() ); }
Right now, when we try to compile it, the compiler will show an error. Basically, the compiler is saying you cannot use a trait function if the trait is not in the scope, and the help from the compiler is showing what we need to do.
- Put the following line above the
main()
function and the compiler should produce a binary without any error:use encryptor::Encryptable;
- Let's try running the executable:
> ./main Input the string you want to encrypt: asdf123 Your encrypted string: nfqs123 > ./main Input the string you want to encrypt: nfqs123 Your encrypted string: asdf123
We have finished our program and we improved it with real-world encryption. In the next section, we're going to learn how to search for and use third-party libraries and incorporate them into our application.