Full Blog TOC

Full Blog Table Of Content with Keywords Available HERE

Sunday, July 30, 2023

Which Software Engineer Should You Recruit?


 

Which software engineer should you recruit to your team? What is the difference between a junior, senior and an ace software engineer? What is the expected salary, and does it worth spending it?


A crucial part of a being a senior software engineer and team leader in a software company is the hiring new personal process. This occurs when you're building up a new team for a new project, when the project expands and requires more developers, and when your need to fill in the gaps of software engineers who went seeking other adventures out of the company.

Before starting a recruiting process, we must understand who are we looking for. Support we want a full stack developer, should we hire a fresh newbie just out of the university, or a software engineer that had been out there working for 2 years? 10 years? What if we happen to encounter a superstar, should we hire him and pay the high cost?


Mixed Team

To answer this we should first examine the current situation in the existing team. A general guideline for an excellent team is to have a team that includes up to 7 employees. Most of the employees in the team should be seniors, which means they should have a good experience of at least 10 years. One or two of the employees should be juniors, with experience of 1-4 years. If you're lucky, you will also have a superstar software engineer in the team, with at least 15 years of experience and excellent analytical and performance abilities.

Why do we need such a team? 

We need such a team to create maximum production while avoiding tension. The difference in the team members skills would create a sharing and teaching habit between the more experienced team members to the others. The superstar can technology guide and lead the team while tackling a huge share of the tasks himself. 

Let's examine each of these typecasts.




The Junior

The junior software developer has 1-4 years of experience. The production output from this team member is expected to be very low. It might be even negative production output:

1. The team needs to invest resources in educating the junior software developer.

2. The mistakes and bugs caused by the junior cause production lost, both when fixed during the development process, and when are discovers as bugs on customer deployments.


Still there are benefits for employing a junior:

1. The need to education and information sharing in the team becomes an integral and a legit process in the everyday working process. This benefits both the juniors, but under the surface provides a benefit for the senior team members as well.

2. The payroll of a junior is low.

3. There are many juniors available in the market

4. The junior would eventually become a senior, and in case he chooses to stay in the company, it is a senior with several year of experience exactly in the development domain you need.

5. In the long term, this is the only effective method to train new software developers, that is - by experience, and as a society we should strive to create senior software developers.


The Senior

The senior software developer has at least 5 years of experience, which usually spent in several companies. The production output from this team member is expected to be high. In case you don'y have a superstar in the team, this is where the majority of work is done. 

A senior in the team is expected to have payroll of 2-3 times more than a junior, and it is harder to find a good senior, and the seniors are less available in the market.

The senior is usually limited to few technology domains, for example: 1-2 programming languages, 1-2 development frameworks.

The Superstar

The superstar software developer has at least 15 years of experience. If you are lucky to get one in your team, then you are in good state. The superstar would both lead the guide the juniors and seniors in the team, and would take the heavy-lifting tasks on his own. A production output from a superstar is at least 5 times fold higher than a senior software engineer.

Why?

The superstar has high development rate and high quality code. This means a lot of output, with very few rejects in form of bugs and customer issues. The architecture of the product would usually also handled by the superstar, reducing mistakes that otherwise would have huge impact on the road-map in the future.

The superstar is not limited by domains, any new programming language, and any new framework, is usually taken over in a short period. In most cases, the superstar adapts and enhances the frameworks to the team needs.

A superstar in the team is expected to have payroll of 2-3 times more than a senior, and it is extremely hard to find one in the market.





Sunday, July 23, 2023

Rust - I give up!




 For the last 2 months I've been trying to use the Rust language. In the beginning it looked promising - get the performance of a C++ code with a better stability, simpler memory management, everything solved at compile time, and not sorrow hours of trying to solve a memory leak/overrun in runtime.

BUT...

As I've stepped through the Rust Book,  I've discovered the complexity of the compiler getting higher and higher. I've spend hours creating a code that in other programming languages I can do in 5 minutes. It make no sense.

Not only that, I've also discovered the "Smart Pointers" chapter, and the formal declaration:

"

Rust’s memory safety guarantees make it difficult, but not impossible, to accidentally create memory that is never cleaned up (known as a memory leak). Preventing memory leaks entirely is not one of Rust’s guarantees, meaning memory leaks are memory safe in Rust.

"

Oh really?? So why should I spend my time on this? As the applications in Rust get more complex, we are forced using smart pointers, and lose the memory magic of Rust. Suddenly I feel much less motivated to spend time on solving complex compilation when I get almost nothing for this.


To sum:

While Rust is great for simple tasks, such as string parsing, regex pattern, and a short lived tasks that do not save object oriented state in the memory, we can use rust. In such case the compiler complexity is somehow relatively small pain. 

In case we want complex state using object oriented design, do not waste time on rust. Instead use Java or Go or even C++. 



Sunday, July 9, 2023

Rust Cargo Workspaces

 



In this post we will review the steps to create a multi workspaces rust project.


For this example we will create a flags parsing library that gets settings from the arguments and from the environment variables, while using defaults if the setting is not configured.


Creating the workspace and crates


First we create a new folder for the workspace, and add the binary create to the project:

mkdir flagger
cd flagger/


Create a Cargo.toml file with the following content:

[workspace]

members = [
"flags_printer",


And then run:

cargo new flags_printer

 

Next we add a library to parse the flags.
We update the root Cargo.toml file:

[workspace]

members = [
"flags_printer",
"flags_parser",
]


and create the library crate:

cargo new flags_parser --lib

lastly we add dependency in flags_printer to the flags_parser in flags_printer/Cargo.toml

[dependencies]
flags_parser = { path = "../flags_parser" }


Creating the parser code

The main.rs:


use flags_parser::FlagsParser;

fn main() {
let mut parser = FlagsParser::new();
parser.configure_flag("runs","1");
parser.parse_flags();
}


The lib.rs:


use std::collections::HashMap;
use std::env;

pub struct FlagsParser {
values: HashMap<&'static str, &'static str>,
}

impl FlagsParser {
pub fn new() -> Self {
return FlagsParser {
values: Default::default(),
};
}
pub fn configure_flag(
&mut self,
name: &'static str,
default_value: &'static str,
) {
self.values.insert(name, default_value);
}

pub fn parse_flags(
&mut self,
) {
let mut last_key = "";
for argument in env::args() {
if last_key != "" {
let value:&str = Box::leak(argument.into_boxed_str());
self.values.insert(last_key, value);
last_key = ""
} else {
for (key, _) in &self.values {
if argument == "--".to_owned() + key {
last_key = key;
}
}
}
}

println!("{:?}",&self.values)
}
}




Monday, July 3, 2023

Rust closures ans iterators

 



Rust provides closures and iterators to enable functional programming. In first look it looks great, like the same nice functional programming in other programming languages, but a deeper check exposes that rust suffers from non friendly requirements. See the following example which demonstrates the weird iterators and closure interactions.



fn main() {
let mut updated_numbers: Vec<i32> = Vec::new();
let numbers = vec![1, 5, 4, 7, 8, 9];

// long and explicit definition of a closure
let decrement_number = |x: &i32| -> i32{
println!("decrement a number");
// here we access a variable from outside the closure scope
updated_numbers.push(x.clone());
x - 1
};

// short hand implicit definition of a closure
let increment_number = |x| x + 1;

// we use iterators to create new vectors
let incremented: Vec<i32> = numbers.iter().map(increment_number).collect();
let back_to_origin: Vec<i32> = incremented.iter().map(decrement_number).collect();


println!("numbers {:?}", numbers);
println!("incremented {:?}", incremented);
println!("and decremented back {:?}", back_to_origin);
println!("we've updated the following {:?}", updated_numbers);


/*
It turns out functional programming in rust is no fun!
iter() - creates a pointer to the items
filter() - creates another pointer to the pointer to the items
hence, we have a double pointer in the filter, very unexpected behavior
*/
let filter_even = |x: &&i32| -> bool { return *x % 2 == 0 };
let even: Vec<i32> = numbers.iter().filter(filter_even).map(|x: &i32| -> i32 { *x }).collect();

/*
This emphasise the complex rust internals.
we must redefine the closures since the order of actions is different
*/
let filter_even = |x: &i32| -> bool { return x % 2 == 0 };
let decrement_number = |x| x -1;
let odd: Vec<i32> = numbers.iter().map(increment_number).filter(filter_even).map(decrement_number).collect();

println!("even numbers are {:?}", even);
println!("odd numbers are {:?}", odd);
}




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());
}