Full Blog TOC

Full Blog Table Of Content with Keywords Available HERE

Monday, June 26, 2023

Grep sample in Rust

 



Following the example here, I've created a sample & simple grep application in rust.


main.rs

use std::{env, process};

fn main() {
let args: Vec<String> = env::args().collect();
let config = rust1::Config::new(&args)
.unwrap_or_else(|err| {
eprintln!("parsing arguments failed: {err}");
process::exit(1);
});

if let Err(e) = rust1::run(config) {
eprintln!("application error {}", e);
process::exit(1);
}
}


lib.rs

use std::{env, fs};
use std::error::Error;

pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}

impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() != 3 {
return Err("invalid arguments amount");
}
let query = args[1].clone();
let file_path = args[2].clone();

let ignore_case = env::var("IGNORE_CASE").is_ok();

Ok(Config { query, file_path, ignore_case })
}
}


pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)
.expect("should be able to read the file");

let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};

for line in results {
println!("{line}")
}
Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();

for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}

results
}

pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();

for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}

results
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";

assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}

#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

assert_eq!(vec!["Rust:", "Trust me."], search_case_insensitive(query, contents));
}
}



Saturday, June 10, 2023

Testing in Rust


 


In this post we show an example of testing in rust.



use std::{thread, time};
fn main() {}

// this is our code
mod toys {
use std::{thread, time};

pub struct Toy<'a> {
name: &'a str,
size: u32,
move_time: u64,
}

impl<'a> Toy<'a> {
pub const fn new(name: &'a str, size: u32) -> Self {
if name.len() == 0 {
panic!("name must be supplied")
}
Self {
name,
size,
move_time: (size / 2) as u64,
}
}
pub fn is_bigger(&self, other: Toy) -> bool {
self.size > other.size
}

pub fn how_long_to_get_over_here(&self) -> u64 {
println!("Come here {}", self.name);
thread::sleep(time::Duration::from_secs(self.move_time));
self.move_time
}
}


pub const DINO: Toy = Toy::new("Rex", 10);
pub const CAR: Toy = Toy::new("Mustang", 4);
}


/*
here we add a new module with config `test`.
this means the code will not be included in our final module.
*/
#[cfg(test)]
mod tests {
// as the tested module is in different scope, we need to explicitly include it
use super::toys;

// test functions are annotated with `test`
#[test]
fn size_matters() {
// we can use assert macro to check results
assert!(toys::DINO.is_bigger(toys::CAR));
}

/*
we can mark test functions not to run by default.
this is required, for example, in case the test runs for a long period
*/
#[ignore]
#[test]
fn calling() {
let seconds = toys::CAR.how_long_to_get_over_here();
/*
1. we use assert_eq here. we can also use assert_ne
2. we also add description the the asser macro. this is displayed in case the assert fails
*/

assert_eq!(1, seconds, "should arrive very quickly");
}

// here we add the should_panic annotation which check for an expected panic
#[test]
#[should_panic]
fn must_use_name() {
toys::Toy::new("", 0);
}
}


Now we run the tests:

$ cargo test
Compiling rust1 v0.1.0 (/home/alon/git/rust1)
warning: unused imports: `thread`, `time`
--> src/main.rs:1:11
|
1 | use std::{thread, time};
| ^^^^^^ ^^^^
|
= note: `#[warn(unused_imports)]` on by default

warning: `rust1` (bin "rust1" test) generated 1 warning (run `cargo fix --bin "rust1" --tests` to apply 1 suggestion)
Finished test [unoptimized + debuginfo] target(s) in 0.26s
Running unittests src/main.rs (target/debug/deps/rust1-6a33c00b5734b10f)

running 3 tests
test tests::calling ... ignored
test tests::size_matters ... ok
test tests::must_use_name - should panic ... ok

test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s


Notice the ignored test are not run. To run the ignore test, we should explicitly ask:

$ cargo test -- --ignored
warning: unused imports: `thread`, `time`
--> src/main.rs:1:11
|
1 | use std::{thread, time};
| ^^^^^^ ^^^^
|
= note: `#[warn(unused_imports)]` on by default

warning: `rust1` (bin "rust1" test) generated 1 warning (run `cargo fix --bin "rust1" --tests` to apply 1 suggestion)
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/main.rs (target/debug/deps/rust1-6a33c00b5734b10f)

running 1 test
test tests::calling ... FAILED

failures:

---- tests::calling stdout ----
Come here Mustang
thread 'tests::calling' panicked at 'assertion failed: `(left == right)`
left: `1`,
right: `2`: should arrive very quickly', src/main.rs:71:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
tests::calling

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 2 filtered out; finished in 2.00s

error: test failed, to rerun pass `--bin rust1`







Monday, June 5, 2023

Rust Generics and Traits


 


This post includes an example demonstrating usage of Generics and Traits in Rust.


use std::fmt::Display;

fn main() {
// trait is similar to interfaces in other languages
trait Hashed {
fn get_hash_key(&self) -> String;
}

// here we have a trait that also includes a default implementation
trait FirstAndLastName {
fn get_names(&self) -> (&str, &str);
fn get_names_last_before_first(&self) -> (&str, &str) {
let (name1, name2) = &self.get_names();
(name2, name1)
}
}

// this is a generic struct, it uses the 'Display' bound to make sure the the T is printable
struct SomethingWithNames<T:Display> {
something: T,
first_name: String,
last_name: String,
}

// here we create an implementation for the generic struct. the return value is the generic type
impl<T:Display> SomethingWithNames<T> {
fn get_value(&self) -> &T {
&self.something
}
}

// this is a function with generic implementation
fn print_something<T: Display>(something: SomethingWithNames<T>) {
let (name1, name2) = something.get_names();
println!("The mystery name of {} is: {}, {}\nThe hash is {}",
something.get_value(), name1, name2, something.get_hash_key());
}

// we implement the hash trait for our first struct
impl<T: Display> Hashed for SomethingWithNames<T> {
fn get_hash_key(&self) -> String {
format!("{}{}{}", &self.something, &self.first_name, &self.last_name)
}
}

// and we implement the hash trait for our second struct
impl Hashed for MyNumber {
fn get_hash_key(&self) -> String {
format!("{}", &self.n)
}
}

// we also implement the names trait for the generic struct
impl<T:Display> FirstAndLastName for SomethingWithNames<T> {
fn get_names(&self) -> (&str, &str) {
(&self.first_name, &self.last_name)
}
}

// here we create 2 different "somethings", based on different types: int and string
let the_answer = SomethingWithNames {
something: 42,
first_name: String::from("the answer"),
last_name: String::from("to everything"),
};

let john = SomethingWithNames {
something: "john",
first_name: String::from("john"),
last_name: String::from("doe"),
};

print_something(the_answer);
print_something(john);


// this is another totally different struct that gets the hash implementation
struct MyNumber {
n: i32,
}

let just_a_number = MyNumber {
n: 13,
};

println!("The hash for just a number is {}", just_a_number.get_hash_key());
}




 



Monday, May 29, 2023

Rust Error Handling

 



In this post we will review Rust error handling methods and best practice.

The first method to handle errors is to use the panic macro. Notice that once panic was run, there is no standard way to recover from it (putting aside the catch_unwind method). The panic macro will stop the program, and unwind through the call stack, release all memory. An example for panic usage follows below.


extern crate core;


fn end_of(text:String) -> String {
println!("checking the text {text}");
if !text.contains(" "){
panic!("no end of string related work here");
}
let (_,end_of_string) =text.rsplit_once(" ").unwrap();
return String::from(end_of_string);
}

fn main() {
println!("{}", end_of( String::from("Hello World!")));
println!("{}", end_of( String::from("NoSpacesHere")));
}


The output of this example is:


checking the text Hello World!
World!
checking the text NoSpacesHere
thread 'main' panicked at 'no end of string related work here', src/main.rs:7:9
stack backtrace:
0: rust_begin_unwind
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library/std/src/panicking.rs:579:5
1: core::panicking::panic_fmt
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library/core/src/panicking.rs:64:14
2: guessing_game::end_of
at ./src/main.rs:7:9
3: guessing_game::main
at ./src/main.rs:15:19
4: core::ops::function::FnOnce::call_once
at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


Note that we have a detailed stack trace, in addition to the error. 


While panic is good for test and examples, in production code we would probably need to avoid terminating our process for each problematic issue. The recommended method is using the Result enum.



extern crate core;

use std::fs::File;
use std::io;
use std::io::Read;

fn end_of(text: String) -> String {
println!("checking the text {text}");
if !text.contains(" ") {
panic!("no end of string related work here");
}
let (_, end_of_string) = text.rsplit_once(" ").unwrap();
return String::from(end_of_string);
}

fn tail_file(file_path: String) -> Result<String, io::Error> {
let my_file = File::open(file_path);
match my_file {
Ok(mut file_handler) => {
let mut file_data = String::new();
let read_result = file_handler.read_to_string(&mut file_data);
return match read_result {
Ok(_) => Ok(end_of(file_data)),
Err(e) => Err(e)
};
}
Err(e) => {
Err(e)
}
}
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("{}", tail_file(String::from("a.txt"))?);
println!("{}", tail_file(String::from("none existing file"))?);
Ok(())
}



The output of this example is:


checking the text aaa bbb ccc
ccc
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }


In the example above we've used some of the Result manipulation methods. 

First, the tail_file function returns a Result enum typed with String and io::Error. 

Second, any call to a function that can return error is following with match arm expression to handle errors.

Third, the main function is using '?' to panic upon errors, but to enable this, we must change the main signature to return Result with a trait.


The tail_file can be also simplified to use the '?' :


fn tail_file(file_path: String) -> Result<String, io::Error> {
let mut file_data = String::new();
File::open(file_path)?.read_to_string(&mut file_data)?;
Ok(end_of(file_data))
}



Notice that in case of error by a result, we are totally blind to the stack trace, which is a very bad practice. To get a full stack of the error from a result, we can choose using the error_stack crate.




Monday, May 22, 2023

Rust Collections

 




This post contains short cheat-sheets examples for the collections: vectors, strings, and hashmap.


Vectors


// vector without initial value must have the type specified
let v1: Vec<i32> = Vec::new();
// type of vector with initialization is automatically derived
let mut v1 = vec![1, 2, 3];

// add values
v1.push(72);

// direct access to element
let second_item = v1[1];
println!("second item is {second_item}");

// direct access will get error if index of of bounds
// let non_existing = v1[9999];


// using get, returns an option
let non_existing = v1.get(9999);
match non_existing {
Some(x) => println!("it's there {x}"),
None => println!("you went too far"),
}

let cell_pointer = &v1[0];
v1.push(73);
/*
immutable borrow error here
after adding an element to the vector, we might had to reallocated space and move the array data
hence the cell pointer is no longer valid
*/
// println!("item {cell_pointer}");


// update loop on vector
for item in &mut v1 {
*item += 100;
}

// read only loop on vector
for item in &v1 {
println!("vector item {item}");
}


Strings

// new empty String
let mut s1 = String::new();


// String from str alternatives
let data = "One method";
let s2 = data.to_string();
let s3 = String::from("Another method");

// updating a string
let mut updated_string = String::from("start");
updated_string.push_str(" and end");
updated_string.push('!');
println!("updated string to {updated_string}");

// concatenation
let con1 = String::from("Foo");
let con2 = String::from("Bar");
let con3 = String::from("!");
let con4 = format!("{con1} {con2} {con3}");
println!("concatenation is {con4}");
let con5 = con1 + " " + &con2 + " " + &con3;
// borrow of moved value error in con5 creation
// println!("concatenation origin {con1}");

// due to unicode issues, we don't access string chars like this s[2]
for c in "יו".chars() {
println!("char {c}");
}
// notice this will be more than 2 bytes, due to unicode
for b in "יו".bytes() {
println!("byte {b}");
}


Hash Maps

use std::collections::HashMap;
let mut workers = HashMap::new();
workers.insert("Alice", 45);
workers.insert("Bob", 34);

// get with default
let alice_age = workers.get("Alice").copied().unwrap_or(0);
let no_one_age = workers.get("No one").copied().unwrap_or(0);
println!("alice is {alice_age} years old, and no one is {no_one_age}");

// loop
for (name, age) in &workers {
println!("worker {name} is {age} years old");
}

// update values
workers.insert("Alice", 46);
let alice_age = workers.get("Alice").copied().unwrap_or(0);
println!("alice is {alice_age} years old now");

// update values only if new key
workers.entry("Alice").or_insert(99);
let alice_age = workers.get("Alice").copied().unwrap_or(0);
println!("alice is (still) {alice_age} years old");

/*
count words by hashmap, while updating values.
notice that we get a pointer to the value, and we update it directly
*/
let mut words = HashMap::new();
for word in "my name is indigo montoya you have killed my father prepare to die".split_whitespace() {
let count = words.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", words);




Monday, May 15, 2023

Rust Modules


In this post we review how to organize a big project into modules and files.

Let review the files structure:




We have two root standard files: The main.rs is the binary starting point and the lib.rs is the library starting point. We can have both, so the crate can be used both as binary and as library.


Each module should (but not must) exists in it own file. Sub modules should be in a sub folder named after their parent module. We have a module named another_game, and its sub module named player.


Let's review usage of the modules in the main.rs file.




First we can see a module within the main.rs, and not in it own file. This is a bad practice, but is possible.

We can access the modules using relative path and absolute path. Looks like the relative path is the more sensible method. A relative path can also access its parent using the super keyword. 

Anything that we want to access from outside the module must be defined as public. Notice that for structs, in case not all the fields are public, we cannot instantiate them, so we must supply a constructor function. Notice that to access an item from outside its module, we must have all the route from the accessing point to the item public.


Let view some of the module code in another_game.rs:




Here again we see the public keyword wherever we want to provide public access. We can see that a struct can have some of its fields public, and some private.







Monday, May 8, 2023

Rust Structures and Enums




 


Structures

Structures can hold multiple fields of multiple types.


struct Animal {
name: String,
legs: u32,
}


Instances of structures can be created, and modified:


let mut insect = Animal {
name: String::from("insect"),
legs: 12,
};

insect.legs = 6;


Builder function can be used, and we can use defaults for fields name to be as the variable names:


fn produce_6legs_animal(name: String) -> Animal {
return Animal {
name,
legs: 6,
};
}

fn main() {
let ant = produce_6legs_animal(String::from("ant"));
}


Move fields from another structure. Notice that move of values from structure means that some of the fields are no longer valid:


fn print_animal(animal: &Animal){
println!("the animal {} has {} legs", animal.name, animal.legs)
}

fn main() {
let caterpillar = Animal {
name: String::from("caterpillar"),
legs: 12,
};

print_animal(&caterpillar);

let butterfly = Animal {
legs: 6,
..caterpillar
};

print_animal(&butterfly);

// compile error - value borrowed here after partial move
print_animal(&caterpillar);
}


Unnamed struct can also be used. Notice the instance creation is using regular parenthesis:


struct Position(String, i32, i32);

fn main() {
let start = Position(String::from("start"), 0, 0);
}


Structs without any fields can also be used:


struct MyTrait;

fn main() {
let my_trait = MyTrait;
}


Moving on to Objects

Structures are used for object oriented design. Using structs and impl blocked we can create multiple constructors and methods. Notice that we can have multiple impl blocks.


struct Person {
name: String,
age: i32,
}

impl Person {
fn new_baby(name: String) -> Self {
return Person {
name,
age: 0,
};
}
fn is_very_old(&self) -> bool {
self.age > 120
}
fn is_older_than(&self, other: &Person) -> bool {
self.age > other.age
}
}


Using the objects is simple.

Again, to avoid move of ownership use pointers for structure when sending it as parameter.


fn main() {
let noah = Person {
name: String::from("Noam"),
age: 950,
};
let einstein = Person {
name: String::from("Einstein"),
age: 76,
};
let baby = Person::new_baby(String::from("Herman"));


let description = if noah.is_very_old() { "very old" } else { "young" };

println!("the person {:?} is {}", noah.name, description);
if noah.is_older_than(&einstein) {
println!("Noah is older than Einstein")
}
}


Debugging 

By adding #[derive(Debug)] attribute to a structure we can print it in a single line using {:?}, and in multiple lines using {:#?}. In addition we can use the dbg! macro to print code position and variable information.

let baby = Person::new_baby(String::from("Herman"));
println!("single line print {:?}", baby);
println!("multiple lines print {:#?}", baby);
dbg!(&baby);

The output is:

single line print Person { name: "Herman", age: 0 }

multiple lines print Person {
name: "Herman",
age: 0,
}

[src/main.rs:26] &baby = Person {
name: "Herman",
age: 0,
}



Enums

Enums can be just a list, or even hold properties similarly to structs.
In addition, enums can be used in match arms.

use crate::Furniture::{Closet, Table};

#[derive(Debug)]
enum Furniture {
Chair,
Table,
Closet { doors: u32 },
}

impl Furniture {
fn open(&self) {
match self {
Closet { doors } => {
println!("open {doors} doors");
}
_ => (
println!("no need to open")
)
}
}
}

fn main() {
let chair = Furniture::Chair;
let closet = Closet { doors: 2 };
println!("{:?}", chair);
println!("{:?}", closet);

chair.open();
closet.open();
}

Option


A special enum is Option, which can hold a value, and can also contain None.
Notice that match expression must be exhaustive, that is - hold all possible values: Some and None.


fn square(i: Option<i32>) -> Option<i32> {
return match i {
Some(x) => Some(x * x),
None => None,
};
}

fn main() {
let two = Some(2);
let none = None;

// prints Some(4)
println!("{:?}", square(two));
// prints None
println!("{:?}", square(none));
}

We can also check value of an option using "if let" statement.

if let Some(i) = two {
println!("the actual value of the option is {}", i)
}