We all have our moments when we want to learn something cool. It struck me this evening and I decided to pick up Rust again. It feels more natural this time due to my more extended knowledge of systems engineering. Now std::program::exit(1)
doesn’t seem like Rust magic anymore but a standard way programs should exit gracefully.
So I decided to implement Two Pointers in Rust. Boy did I forget what two pointers was. I had to ask GPT to describe two pointers in 10 words. It said something along the lines of, find two numbers in a list that sum to a target number. Now, I chose to design my program to receive all the inputs as program args. The first thing I learnt to do was collect a program’s inputs. Luckily Rust has a handy way in the standard library: std::env::args().collect()
. This creates a vector of all passed arguments. Now the next thing to learn was how to parse the collected arguments. Rust prefers pattern matching to for-loops. So I picked up this piece of code from ChatGPT:
for i in args[2..].iter() {
match i.parse::<i32>(){
Ok(num) => {
println!("{:?} is a number", num);
},
Err(_) => {
println!("Supplied a non-integer {:?}. Exiting...", i);
std::process::exit(0);
}
}
}
Or something along those lines, lol. Writing this code quickly made me realize that Rust REQUIRES SEMICOLONS. What? Modern languages don’t need that shit. That requirements is in the realms of C and C++. Well, Rust does. Except when you are returning from an expression, like the main function of a pattern-matching block. Weird. Anyway, fast-forward to me reading the docs of the HashSet(Map). Hehe, I read that of the HashMap first then quickly realized I could solve 2-sum with less memory requirement if I used a hashset. So, Rust’s HashSet has a very simple API. First you initialize it: let mut hashset = std::collections::HashSet::new();
then you insert values or check for their existence: hashset.insert("something");
, hashset.contains("something");
. And we that, we have a complete 2-sum implementation. Here’s the complete code btw:
use std::env;
use std::collections::HashSet;
fn main(){
let args: Vec<String> = env::args().collect();
let mut number_to_sum = 0;
match args[1].parse::<i32>() {
Ok(num) => {
number_to_sum = num;
}
Err(_) => {
println!("First value {:?} is not an integer. Exiting...", args[1]);
std::process::exit(1);
}
}
let mut nums = Vec::new();
for i in args[2..].iter() {
match i.parse::<i32>(){
Ok(num) => {
nums.push(num);
}
Err(_) => {
println!("{:?} is not an integer. exiting...", i);
std::process::exit(1);
}
}
}
let mut seen = HashSet::new();
for i in &nums {
if seen.contains(&(number_to_sum - i)){
println!("Found pair: ({}, {})", number_to_sum - i, i);
return;
}
seen.insert(i);
}
println!("Found no pairs man")
}
I know, I know rookie mistakes. I eventually replaced the unecessary assigning and reassignment with:
let number_to_sum = match args[1].parse::<i32>() {
Ok(num) => num,
Err(_) => {
println!("First value {:?} is not an integer. Exiting...", args[1]);
std::process::exit(1);
}
};
Improving the Code
I asked ChatGPT to brutally critique the code and it pointed out that I was not failing early enough when getting the first number, i.e., number_to_sum. That’s a rookie mistake, ngl. So I changed that to use args.get
let number_to_sum = match args.get(1).and_then(|x| x.parse::<i32>().ok()){
Some(number) => number,
None => {
if args.len() == 1 {
println!("No argument provided");
std::process::exit(1);
} else {
println!("First value {:?} is not an integer. exiting...", args[1]);
std::process::exit(1);
}
}
};
Then the hashmap inserts… Turns out iteration uses pointers not actual numbers, so I did: seen.insert(*i)
. That’s it. A more optimal 2-sum with Rust. Probably not the best. I’ll improve with time. Below is the complete code:
use std::env;
use std::collections::HashSet;
fn main(){
let args: Vec<String> = env::args().collect();
let number_to_sum = match args.get(1).and_then(|x| x.parse::<i32>().ok()){
Some(number) => number,
None => {
if args.len() == 1 {
println!("No argument provided");
std::process::exit(1);
} else {
println!("First value {:?} is not an integer. exiting...", args[1]);
std::process::exit(1);
}
}
};
let mut nums = Vec::new();
for arg in args[2..].iter() {
match arg.parse::<i32>(){
Ok(num) => {
nums.push(num);
}
Err(_) => {
println!("{:?} is not an integer. exiting...", arg);
std::process::exit(1);
}
}
}
let mut seen = HashSet::new();
for i in &nums {
if seen.contains(&(number_to_sum - i)){
println!("Found pair: ({}, {})", number_to_sum - i, i);
return;
}
seen.insert(*i);
}
println!("Found no pairs man")
}
Solving the isPalindrome problem
isPalindrome
requires that you find a string that is same both rightwards and leftwards. I first solved it in Python and used that Python implementation in Rust. I had thought Rust didn’t have for
and while
loops, but I was wrong. It does have both, in addition to its loop
which I am yet to use. I have seen that Rust just requires that you know that you are a programmer and use the teachings from the standard library to achieve what you want. All the noise around it is unecessary tbh.
Anyway, this was my initial implementation
func is_palindrome(string: &str){
let mut l = 0;
let mut r = len(string) - 1;
loop {
if &string[l] != &string[r] {
return false;
}
l += 1;
r -= 1;
}
true
}
func main(){
let string = "Hannah";
println!("is {:?} a palindrome?: {:?}", string, is_palindrome(string));
}
I knew it didn’t follow all the rules of Rust, so I asked ChatGPT for corrections, and I got a corrected solution thus:
fn is_palindrome(string: &str) -> bool {
let chars: Vec<char> = string.chars().collect();
let mut l = 0;
let mut r = chars.len() - 1;
while l < r {
if chars[l].to_ascii_lowercase() != chars[r].to_ascii_lowercase() {
return false;
}
l += 1;
r -= 1;
}
true
}
fn main(){
let string = "Hannah";
println!("is {:?} a palindrome?: {:?}", string, is_palindrome(string));
}
Right now, the solution above only caters to single-world palindromes. It should be extended. How? We skip special characters and spaces using is_alphanumeric
. Here’s how:
fn is_palindrome(string: &str) -> bool {
let chars: Vec<char> = string.chars().collect();
let mut l = 0;
let mut r = chars.len() - 1;
while l < r {
while !chars[l].is_alphanumeric() {
l += 1;
}
while !chars[r].is_alphanumeric() {
r -= 1;
}
if chars[l].to_ascii_lowercase() != chars[r].to_ascii_lowercase() {
return false;
}
l += 1;
r -= 1;
}
true
}
fn main(){
let string = "Do geese see God?";
println!("is {:?} a palindrome?: {:?}", string, is_palindrome(string));
}
If you check, I used a while loop to move the pointer left or right depending on the presence of alphanumeric characters. This gives an almost perfect solution to the problem. The only issue is that we are doing lots of manual checks, but Rust offers a way to combine the is_alphanumeric
and to_ascii_lowercase
checks into map and filter runs. We simply run those on the string.chars()
before running collect()
. Since we are improving things, we may as well ensure the program can accept input. Let’s use the std::args::collect
function for that. Below is the final code with all the optimization:
fn is_palindrome(string: &str) -> bool {
let chars: Vec<char> = string
.chars()
.map(|v| v.to_ascii_lowercase())
.filter(|v| v.is_alphanumeric())
.collect();
let mut l = 0;
let mut r = chars.len() - 1;
while l < r {
if chars[l] != chars[r] {
return false;
}
l += 1;
r -= 1;
}
true
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() == 1 {
eprintln!("No arg supplied. Exiting...");
std::process::exit(1);
}
let joined = args[1..].join(" ");
let are_args_palindrome = is_palindrome(&joined);
if are_args_palindrome {
println!("{:?} is a palindrome ✅", joined);
} else {
println!("{:?} is not a palindrome ❌", joined);
}
}
I formated too with cargo fmt
. The final built and stripped output is 376 kb. A reasonable size for a simple CLI program. All in all, I can say that Rust is a powerful tool and I’ll continue writing programs, using its features, and building on the ecosystem. I might even become a fanboy.
Building a simple calculator (13 May 2025)
Before you go ahead building airplanes, you should have built a paper-plane, don’t you think? lol. Today I decided to build a calculator. Maybe in exploring this, I will learn about generics. First I wrote the basic code for adding two numbers:
fn add(num1: i32, num2: i32) -> i32 {
num1 + num2
}
fn main(){
println!("The addition of 1 and 2 gives {}", add(1, 2));
}
Going further, how do we ensure that we can add two numbers whether they are floats or ints? Generics! So here’s a basic generics implementation:
use std::ops::Add;
fn add<T: Add<Output = T>>(num1: T, num2: T) -> T {
num1 + num2
}
fn main(){
println!("The addition of 1.0 and 2.0 gives {}", add(1., 2.1));
println!("The addition of 3 and 4 gives {}", add(3, 4));
}
In the snippet above, we supply same types to the add function and it just works. But what if we supply separate types? Do we typecast? Do we make our generics handle both cases? Let’s see the typecasting with a subtract function:
use std::ops::{Add, Sub};
fn add<T: Add<Output = T>>(num1: T, num2: T) -> T {
num1 + num2
}
fn subtract<T, U>(num1: T, num2: U) -> f64
where
T: Into<f64>,
U: Into<f64>,
{
num1.into() - num2.into()
}
fn main(){
println!("The addition of 1.0 and 2.0 gives {}", add(1., 2.1));
println!("The addition of 3 and 4 gives {}", add(3, 4));
println!("The subtraction of 10 and 8. gives {}", subtract(10, 8.0));
}
The above code works because we use the Into
trait to convert from a generic into a float64 number. That’s it really.
Dealing with more than two numbers
When faced with > 2 numbers, it makes sense to perform a functional programming style map, filter, reduce on them. In this particular case, a reduce, which rust implements as fold
, will be the most appropriate. Rust also implements summing in a Sum trait that is part of iterators, but that is aside. Here’s an implementation of using fold from args collected as input:
struct Calculator;
impl Calculator {
fn add(numbers: Vec<f64>) -> f64 {
numbers.iter().fold(0.0, |acc, num| acc + num)
}
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let numbers: Vec<f64> = args[1..]
.iter()
.map(|v| {
if v.parse::<f64>().is_err() {
eprintln!("Error: {} is not a number", v);
std::process::exit(1);
} else {
v.parse::<f64>().unwrap()
}
})
.collect();
let numbers_as_string: String = args[1..]
.iter()
.fold(String::new(), |acc, cur| acc + cur + ", ");
println!(
"The addition of {} gives {}",
numbers_as_string,
Calculator::add(numbers)
);
println!("The subtraction of 10 and 8. gives {}", subtract(10, 8.0));
}
Here, we use static method, sum
on the Calculator struct. We could as well do this with modules:
======operators.rs=======
pub fn add(numbers: Vec<f64>) -> f64 {
numbers.iter().fold(0.0, |acc, num| acc + num)
}
pub fn subtract<T, U>(num1: T, num2: U) -> f64
where
T: Into<f64>,
U: Into<f64>,
{
num1.into() - num2.into()
}
======main.rs=======
mod operators;
fn main() {
let args: Vec<String> = std::env::args().collect();
let numbers: Vec<f64> = args[1..]
.iter()
.map(|v| {
if v.parse::<f64>().is_err() {
eprintln!("Error: {} is not a number", v);
std::process::exit(1);
} else {
v.parse::<f64>().unwrap()
}
})
.collect();
let numbers_as_string: String = args[1..]
.iter()
.fold(String::new(), |acc, cur| acc + cur + ", ");
println!(
"The addition of {} gives {}",
numbers_as_string,
operators::add(numbers)
);
}
Detouring
I asked ChatGPT for a better implementation of the code. Turns out, I didn’t need to use fold and map, most especially the fold in main.rs. Rust already provides elegant ways to handle things like skipping the first argument. It also provides a join method. Feels like Python tbh. Here’s a more elegant representation:
mod operators;
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let mut numbers = Vec::new();
for arg in &args {
match arg.parse::<f64>() {
Ok(n) => numbers.push(n),
Err(_) => {
eprintln!("Error: '{}' is not a valid number", arg);
std::process::exit(1);
}
}
}
let numbers_as_string = args.join(", ");
println!(
"The addition of {} gives {}",
numbers_as_string,
operators::add(numbers)
);
}
I should really be using features more. Welp, step by step. That’s all for now.