Rust Ramblings: Learning Rust 2
Contents: Recursion, Lists, Basic I/O, Vec, Ownership, Errors and Exceptions
Apparently Rust is the future so I thought I would hop on board. This is more of a reference for myself, than a tutorial. This is part 2, it is more or less organized by the order above.
Ex 1: Basic I/O
Write a program that asks the user for their name and greets them with their name.
use std::io;
fn main() -> io::Result<()> {
let mut input = String::new();
println!("Enter your name:");
io::stdin().read_line(&mut input)?;
println!("Hello {}!", input.trim());
Ok(())
}
- function main returns Result type, a specialized type for I/O operations, used for operations that may produce an error.
- let - by default creates an immutable variable (cant be changed)
- let mut - mutable variable
- String::new() - an empty and growable String, heap-allocated
- stdin(&mut input).read_line - read a line from stdin, and append it into a string without overriding its content. This string is input, mut indicates that the input can be modified
- trim() - strip new line
crazyeights@es-base:~/Desktop/RustyBucket$ ./ex1
Enter your name:
Joe
Hello Joe!
crazyeights@es-base:~/Desktop/RustyBucket$
Ex 2: Recursion
Write a function that converts a number to binary
Recursion in Rust is no different than recursion in any other language:
use std::io;
fn main() {
let n = get_number();
let b = convert(n);
println!("Binary: {}", b);
}
fn convert(x: i32) -> i32 {
if x == 0 {
x
}else{
x%2 + 10 * convert(x/2)
}
}
fn get_number() -> i32 {
let mut input = String::new();
println!("Enter a number:");
io::stdin().read_line(&mut input)
.expect("can not parse input");
let n = input.trim().parse().unwrap();
n
}
Ex 3: Lists II
Modify the previous program (sum of an array) such that only multiples of three or five are considered in the sum, e.g. 3, 5, 6, 9, 10, 12, 15 for n=17
fn main() {
// Declare and Initialize a list of size 15
let arr: [i32; 15] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
// Create a mutable variable sum, set to 0 to hold the sum of the list
let mut sum = 0;
// Iterate through the list, if the current element is divisible by 3 or 5, add it to the sum:
for i in 0..arr.len(){
if arr[i] % 3 == 0 || arr[i] % 5 == 0 {
sum+=arr[i];
}
}
println!("The sum of the array is: {}", sum);
}
Ex 4: Vec
The Vec type is a dynamic array, that can grow and shrink in size. It is heap-allocated. Here are some sample operations:
// Vectors:
// A vector is represented using 3 parameters:
// pointer to the data
// length
// capacity - how much memory is reserved for the vector, maximum size
fn main() {
// Init an empty vector
let mut a: Vec<i32> = Vec::new();
// Push an integer to the vector
a.push(1);
// Print the vector:
println!("{:?}", a);
// Push 3
a.push(3);
// a = [1, 3]
println!("{:?}", a);
//Print the first element
println!("{}", a[0]);
//Remove an element from the array
let a1 = a.pop();
println!("{:?}", a1);
a.push(5);
// Empty the vector, pop and print each vector
while let Some(top) = a.pop() {
println!("{}", top);
}
}
Since Rust is a functional-ish language, let’s attempt to perform the three higher order functions on a vector, these being accumulation, transformation, and selection.
Accumulation:
// Accumulation (Folding) - apply a binary operator between the elements of a list
fn main() {
// Initialize a vector
let v = vec![1,2,3,4,5,6];
// Get the sum of the list:
let sum = v.iter().fold(0, |total, next| total+next);
println!("Sum: {}", sum);
}
Transformation:
// Transformation (Mapping) - call a function on every element in a list
fn main() {
// Initialize a vector
let mut v = vec![1,2,3,4,5,6];
// Double all elements in the list:
v = v.iter().map(|&x| 2*x).collect();
println!("{:?}", v);
}
Selection:
// Selection(Filtering) - Test a predicate for every element of a list, keep only elements where the predicate is true:
fn main() {
// Initialize a vector
let mut v = vec![1,2,3,4,5,6];
// Selection (Filter) - get all even elements:
v.retain(|&i|i % 2 == 0);
println!("{:?}", v);
}
When we combine these operations, we can perform more complex operations very simply:
// Return a list of even values multiplied by 2
fn main() {
// Initialize a vector
let mut v = vec![1,2,3,4,5,6];
v = v.iter().filter(|&i| i % 2 == 0).map(|&x| 2*x).collect();
println!("{:?}", v);
}
Searching a list:
We can use iter(), and position() to find the indices of a particular list item:
fn main(){
let a = vec![1, 2, 43, 5, 9, 12];
let index = a.iter().position(|&i| i == 9).unwrap();
println!("{}", index);
}
Ownership
Each value has a variable called its owner. A value can only have one owner at a time. When its owner goes out of scope the value will be dropped and memory freed. A variable remains valid until it goes out of scope (like any other programming language) An example of scope:
fn main() {
{
let a = 1;
println!("{}", a);
}
println!("{}", a); // error - out of scope
}
Rust automatically returns memory once the variable that is using it goes out of scope, this avoids mistakes with freeing memory, and garbage collection.
// When we have a variable x, and we set:
let y = x;
/* when x is a basic type (integer, chars, bool, stored on the stack, with a fixed known size), Rust creates a copy of x
when x is a an object (String, Vec, ...), Rust creates a pointer to the object, where y refers to x. */
For example:
fn main() {
// When we have:
let x = 2;
let y = x;
// We can still access both x, and y:
println!("x: {}, y: {}", x, y);
// When we have:
let a = String::from("hello");
let b = a;
// When we try to print a:
println!("{}", a);
// We get: error: borrow of moved value: `a`
}
The operation let b = a;
can be described as moving the value of a to b
If we want to keep a we could use clone:
let b = a.clone();
With &
we can refer to a value without taking ownership of it (called borrowing)
fn main() {
let a = String::from("hello");
let b = &a;
//No error:
println!("{}", a);
println!("{}", b);
}
Errors and Exceptions
Panic
Panic is for errors that should never happen, they are unrecoverable errors. Calling panic allow for a program failure to occur at will. It is meant for tests, and unfinished implementations. For example:
// Unfinished Function
fn unfinished_impl(a: i32) {
if a < 2 {
panic!("Unfinished!");
} else {
println!("All is good!");
}
}
fn main() {
unfinished_impl(3);
unfinished_impl(1);
}
This causes the following output:
crazyeights@es-base:~/Desktop/RustyBucket$ ./ex_errors
All is good!
thread 'main' panicked at 'Unfinished!', ex_errors.rs:19:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Option
The Option type is for when a value is optional, or when the lack of a value is not an error condition. It is similiar to Haskells Maybe type.
// Option<T> - in the std library, when absence is a possibility
// Some(T) - an element of type T was found
// None - no element was found
// These cases are handled via match
// implicitly via unwrap
fn main() {
safe_division(12, Some(4));
safe_division(12, Some(0));
safe_division(12, None);
}
fn safe_division(num: i32, divisor: Option<i32>) {
match divisor {
Some(0) => println!("Divison Error"),
Some(inner) => println!("Result: {}", num/inner),
None => println!("No Divisor")
}
}
This program has the following output:
crazyeights@es-base:~/Desktop/RustyBucket$ ./ex_errors
Result: 3
Divison Error
No Divisor
Result:
Result is a type that describes an error, that is to be handled by the caller. This includes things like unexpected input formats, and unexpected return values. Here is a basic example (not written by me, Source: https://doc.rust-lang.org/rust-by-example/error/result.html), where when parsing an integer from a string, an integer is not present in the string.
// Result:
// Type that describes an error, instead of an absence
// 2 Outcomes:
// Ok(T) - an element of type T was found
// Err(E) - an error was found with element E
// Example: parsing a integer, from a string
use std::num::ParseIntError;
fn main() -> Result<(), ParseIntError> {
let number_str = "10";
let number = match number_str.parse::<i32>() {
Ok(number) => number,
Err(e) => return Err(e),
};
println!("The number is {}", number);
Ok(())
}
Unwrap and Expect:
Unwrap handles errors without requiring the user to check for each case. It handles different outcomes as follows: If an Option type has Some value, or Result type has Ok value the program continues. If an Option type has None value, or Result type has Err values the program panics, and terminates.
fn main() {
let x = foo(true).unwrap();
println!("{}", x);
let y = foo(false).unwrap(); // throws an error here
println!("{}", y);
}
fn foo(a: bool) -> Option<i32> {
if a {
Some(2)
} else {
None
}
}
crazyeights@es-base:~/Desktop/RustyBucket$ ./ex_unwrap
2
thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', ex_unwrap.rs:6:24
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Expect is like unwrap, and is used to set custom error messages:
// expect snippet
fn main() {
let e: Result<i8, &str> = Err("some message");
e.expect("an error has occured.");
}
These notes are not exhaustive, I am just scratching the surface here. I am just a beginner, so for more complex applications there would probably be more extensive and exhaustive error handling.
I will tackle structs next.