Learning Rust
Last edited 3 hours, 53 minutes ago
Attempting to learn rust and will dump random snippets, learnings, etc. here for posterity :)
I do not recommend anyone actually read this, publishing purely for funsies and my own self-reference. I learn best when engaging with material, and for a programming language that's me futzing around in an IDE and comparing to other languages.
first program
From first couple chapters of The Rust Programming Language, a simple number guessing game with annotations as I futzed around in my IDE.
use std::cmp::Ordering;
use std::io;
use rand::RngExt;
fn main() {
println!("Hello, world!");
let x = 5;
let y = 10;
println!("X: {x}, Y: {y}");
println!("Guess the number!");
// lol wtf is this. random_range accepts a SampleRange, guess that's a trait?
// The =blah part makes it high inclusive, omitting = leave it high exclusive.
let secret_number = rand::rng().random_range(1..=100);
// Lmao 'loop' is the designated keyword for infinite loops, however 'while true' does also run.
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
// At this point guess is a String, but most methods on it seem to return &str. Not sure of
// the difference yet.
// &str has a method to_string() to return to a String.
// Conversely, String has a method as_str() to convert to a &str.
// 2 types of doing this. Guess if you don't indicate to the compiler the var is u32
// it would fail, so you either need to give a type annotation (bottom) or use the
// "turbofish syntax so the parse generic type T can be resolved.
// let guess = guess.trim().parse::<u32>().expect("Please type a number!");
// let guess: u32 = guess.trim().parse().expect("Please type a number!");
// Seems enum values can take args, or perhaps these are because Result is generic?
// Don't entirely follow since Ordering used below is just consts.
let guess = match guess.trim().parse::<u32>() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
},
}
}
}
Collections and statements/expressions
In addition to below tinkering, learned about workspaces which I'm using locally so I can keep creating new programs in the same repo. Decided to manage separate packages for each, which are then addressable by name using cargo run -p <pkg>. Have a directory tree like
collections/
src/
main.rs
Cargo.toml # describes pkg - `[package]` & `[dependencies]`
guessing_game/
src/
main.rs
Cargo.toml # describes pkg - `[package]` & `[dependencies]`
Cargo.lock
Cargo.toml # contains `[workspace]` w/ resolver = '3'
fn main() {
println!("Hello, world!");
we_have_tuples();
we_got_arrays_too();
println!("expression evaluated to: {}", statements_vs_expressions());
}
fn we_have_tuples() {
// mixed types, fixed length
let tup: (&str, i32) = ("foo", 32);
// string literals are of type &str. Below fails
// let s: String = "blah";
println!("tup: {:?}", tup); // idk what this is, can't print a tuple without :? though.
// Something about Display trait?
// pattern matching destructuring :)
let (s, i) = tup;
println!("i: {}, s: {}", i, s);
// direct element access with whatever this is
println!("First value in the tuple is '{}'", tup.0);
}
fn we_got_arrays_too() {
// fixed length, guessing we have vectors or somethin' for dynamic
let _ = [1, 3, 5, 7, 9];
// EDIT: like 2 paragraphs down in the rust book, yeah we got vectors :cool:
// ayooo initialize to a known value
let _ = [2; 10]; // len 10, all initialized to 2
// unrelated, seems some methods/traits don't work if type is inferred? This won't compile
// > can't call method `abs` on ambiguous numeric type `{integer}`
// let a = 94.abs();
let a: i32 = 94;
println!("a abs: {}", a.abs());
// iterators!!!
let even_nums = [2, 4, 6, 8, 10];
for even in even_nums.iter() {
println!("even: {}", even);
}
// interestingly seems `for _ in _` works plainly on arrays. Something about borrow/consume
for even in even_nums {
}
}
fn statements_vs_expressions() -> i32 {
// statements end with semicolons, expressions don't
// branching expressions must always return the same type
let _ = if 5 > 0 {
"oh yeah that is a positive number"
} else {
"wooahhh that is negative"
};
// above would apply to assignment from a match expression as well.
// This also means 'if' is an expression in rust, other languages I know just happen
// to have it as a statement.
let val = {
let temp = 0;
temp - 1 // this boi is an expression, implicit returns :(
};
val // could also inline above {} block and remove trailing semicolon
}
Ownership
Aaand it's time to learn the borrow checker. Seems pretty intuitive but we'll see how multithreading later complicates it :)
The restrictions around not having multiple references at the same time makes sense but will continue tripping me up until my IDE tells me I'm being stupid. Same applies to inability to mix immutable and mutable references for the same data.
fn main() {
let mut s = String::from("greetings");
s.push_str(", traveler"); // or `push` for a single char
println!("s:\t{}", s);
let s2 = s;
println!("s2:\t{}", s2);
// println!("orig: {}", s); // fails, `value borrowed here after move`
{
let s = "foo";
} // s goes out of scope, `drop` called
// String is on the stack but contains a pointer to heap, len, & capacity. Similar to Go slices
// rust will only create shallow copies ("move") unless you explicitly reach for deep copying
let mut s3 = s2.clone();
s3.push('!');
println!("s3:\t{}", s3);
// Integers and other 100% stack-bound types can have the Copy trait which leaves the
// orig value usable. Copy trait can only be on a type which doesn't have the Drop trait,
// nor any of its components
let x = 100;
let y = x;
println!("x: {}, y: {}", x, y); // see, this works
// simple scalars have Copy, as do tuples composed of only Copy-able types
{
let s = String::from("foobar");
takes_ownership(s);
// println!("takes_ownership(s): {}", s); // invalid - argument passing does a move/copy
}
{
let s = gives_ownership();
println!("s: {}", s); // we own this now (as you'd expect)
}
{
let s = String::from("hello");
// Like C, we got pointer references and dereferencing. Refs do not take ownership
println!("len of '{}': {}", s, borrow(&s));
}
{
let mut s = String::from("omg"); // this gotta be mutable to begin with
borrow_mutate(&mut s); // ref needs to be mutable too
println!("s after borrowed mutation: {}", s);
// only one mutable ref to a piece of data is allowed in a scope. Below fails
let r1 = &mut s;
let r2 = &mut s;
// lol no it doesn't book is outdated. Apparently now permitted and borrow checker
// uses "non-lexical lifetimes". Borrow lives until last use. THIS fails though,
// you still can't have multiple mutable refs at the same time. Difference is that
// before (without below usage of r1/r2), r1 expired as r2 was initialized.
// println!("r1: {}, r2: {}", r1, r2);
}
{
// cannot have a mutable ref while an immutable one exists
let mut s = String::from("hello");
// fails for similar reason as prior block
// let r1 = &s;
// let r2 = &mut s;
// println!("r1: {}, r2: {}", r1, r2);
// this however is fine
let r1 = &s;
let r2 = &s;
println!("r1: {}, r2: {}", r1, r2);
}
{
let mut s = String::from("i got a feelin");
let word = first_word_slice(&s);
// fails - already have an immutable ref so extending the mutable ref to here
// would cause issues
// s.clear();
println!("first word of \"{}\" is {}", s, word);
}
}
fn takes_ownership(s: String) {
println!("takes_ownership: {}", s);
}
fn gives_ownership() -> String {
let s = String::from("feelin' crabby");
if 5 > 0 {
// idk why this needs `return` but it does. Without, below usage complains use after move
return s;
}
s
}
fn borrow(s: &String) -> usize { // refs as func params is called "borrowing"
s.len()
// A ref going out of scope does not invoke `drop`
}
fn borrow_mutate(s: &mut String) {
s.push('!')
}
// unrelated to chapter, funcs are values :)
fn foo(f: fn()) -> String {
String::from("foo")
}
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
// '&str' is a "string slice". Seems this is functionally different from, say, i32 which is just
// an int pointer/ref. Stems from ints being a sized type (known size) whereas str is unsized
// (size unknown at compile time)
fn first_word_slice(s: &String) -> &str {
// slices track ownership and are invalidated if the parent resource mutates
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i]; // like Go sub-slicing, can omit start/end idx
}
}
&s[..]
}