The Rust Programming Language
These are my notes as I go through the main Rust book.
My goal is that these notes be a “living” document. As I program more and more in Rust, I will update these notes. I will remove the obvious stuff people can grasp quickly. I will then add more info on nuances I uncover.
1 Getting Started
https://doc.rust-lang.org/book/
These are the interesting tidbits that stand out from the book.
Rust is an “expression based” language. File extension is .rs.
rustc rustfmt rustdoc
2 Programming a Guessing Game
This introduces a basic REPL.
Why is std::cmp::Ordering used for basic comparison of numbers?
.expect() and Result<T, E>
Result<T, E>is a type-parameterized enum.
These kinds of enums have variants of Ok<T> and Err<E>.
.expect() may be called on a Result<T> type.
This will either unwrap it or fail with a message.
{var} is the “crab pincer” syntax for println!
3 Common Programming Concepts
3.1 Variables and Mutability
const is basically constexpr; its value/definition must be known at compile time.
Normal variables are constants (immutable).
mut are “normal”, mutable variables.
Variable Types
Mutable
let mut <var_name> = <value> for a variable proper
Immutable
let <var_name> = <value> for an immutable.
These are clutch in applying transformations through shadowing.
Constants
Compared to variables (even immutable), constants can only be set to a constant expression, and they can be declared in any scope.
Shadowing lets you:
- Reuse names.
- Hide values in higher-scopes when you might be using a value derived from it in a narrower scope.
- “Change” the type of the variable (cannot do this by mutating a variable).
- Basically mutate it, but it is explicit, and it becomes implicitly immutable again immediately.
3.2 Data Types
isize and usize are word-length for the compile target’s architecture.
A byte literal b'A' is specifically for u8 bytes.
When compiled with --release, integers will overflow.
Otherwise the program panics.
Overflowing can be handled differently with methods:
wrapping_*checked_*overflowing_*saturating_*
Rust’s char is 4 bytes and represents a Unicode scalar value.
It ranges from U+0000 to U+D7FF and U+E000 to U+10FFFF inclusive.
Ranges
inclusive-exclusive: START...END
inclusive-inclusive: START...=END
Tuples
Tuples are a primitive, compound type and allow destructuring. Indexing is also possible with a dot operator.
#![allow(unused)]
fn main() {
let tup = (1, 2.0, false);
let (x, y, z) = tup;
let y = tup.0;
}
The () empty tuple is called unit.
This is like void in that it is returned implicitly if there are no other returns.
Arrays
Arrays are defined on the stack and have a fixed length. Otherwise you will need a vector.
Arrays can be initialized either as [type; length] or [value; length] with
the latter’s value being repeated.
3.3 Functions
Parameters must have a type annotation.
fn foo(some_var : i32){}
Expressions’ return value is their final expression.
{
let x = 2;
x + 6
} // returns 8
Of course, these are used as function bodies.
3.4 Comments
// comments can be inline.
/// are documentation comments (Markdown-ready which compile to HTML through Cargo).
Panics, Errors, Safety, and Examples are common headings.
//! adds commentary to the containing item (such as a crate or module) rather than the below piece of code.
3.6 Macros
fn foo!(){}
dbg!() is a useful macro to print debug information of arguments and simultaneously return ownership.
3.5 Control Flow
if is an expression (with “arms” like match) so it may be used as a ternary.
let number = if condition { 5 } else { 6 };.
Because of this, all arms must return the same type.
Loops
loop, while, and for
for is basically an enhanced for loop in C/++.
<LOOP> [expression] {
<STATEMENT>; {STATEMENTS}...
}
Loops can have labels.
'label: loop {
}
You can break on these so to not break on the innermost loop.
break 'label;
You can return a value to make a loop an expression on a break.
break some_val;
With loops, a value may be added to the end of the break statement to return a value.
This means the loop is an expression and can be assigned from.
Loops may have labels 'label: loop { ...; break 'label; } to specify non-innermost breaks.
while loops are also available.
for loops are more like C++ enhanced for loops.
They’re also like Python for loops with a range type.
Ranges are provided by the standard library and apparently the literal looks like (x..y).
4 Understanding Ownership
4.1 What is Ownership?
String literals are on the stack, but Strings are allocated to the heap.
Thus string literals are constexpr, basically.
Because Strings are on the heap, String::push_str(self, str) is the name of the appending function.
Memory is automatically returned once the variable that owns it goes out of scope.
This pattern is called RAII (resource acquisition is initialization) in C++.
The actual “free” function called in rust at the end of scope is called drop.
This means there must be only one “owner”.
Double free error: Another pointer to the same data on a heap goes out of scope.
Assigning one variable to another is actually an ambiguous operation.
On the heap:
Instead of shallow copys (copying structured data and pointer values), rust performs moves.
The first variable becomes invalidated.
Rust never automatically performs deep copies (clone()) which would copy all member pointers blocks of memory.
Some data implements Clone though, then .clone() will perform a deep copy.
stack-only data:
If all data is on the stack, there is no difference between a deep and shallow copy.
Thus, if stack-only data implements copy(), no move occurs, just copy semantics.
The primitives types and tuples of only primitives implement the Copy trait.
Ownership and Functions
Passing arguments to parameters uses the exact same ambiguous semantics as variable assignments.
If anything is moved in rather than copied in, the drop at the end of the function scope could be problematic. You could return a tuple with the extra values of anything moved in to move it back out. That’s “too much ceremony” though, so you get references.
4.2 References and Borrowing
You can have as many immutable references to a variable as you like. Once you have an immutable reference, that’s the only reference you can have of it for the remainder of its scope.
A scope of a reference ends with its last usage, so a scope is not strictly a syntactic block. It can be confusingly more forgiving.
4.3 The Slice Type
A “string slice” is &str and the literal looks like &s[0..len].
The inclusive beginning and exclusive ending can be omitted, i.e. &s[..].
Indices can be variables, e.g. &s[2..i].
Immutable borrows of slices help ensure that the underlying data is not mutated later. This is as opposed to calculating some index and storing it in an unrelated variable.
Slicing can be performed on either &String or string slices &str.
Deref coercion can implicitly convert a string reference to a string slice.
References to strings are equivalent to whole slices of strings. String literals are string slices.
There is a type for a general slice of a collection.
It has a type of the form &[i32].
One can take a slice of a string or an array.
A string slice is &str (not to be confused with the more specialized &String).
An array slice type is something like &[i32].
An array slice literal is something like &a[1..3]
4.4 Cloning
Copying stack allocations (most primitives) gives you a copy.
Copying heap allocations (boxes/pointers) gives you a shallow copy.
It also invalidates the original copy.
To do a deep copy, call .clone().
5 Using Structs to Structure Related Data
Field Init Shorthand lets us pass arguments that match the name of a struct field without specifying the struct field.
fn factory(some_field : fieldType) -> Some_Struct {
Some_Struct {
some_field,
some_field2 : some_field_other,
}
};
Struct update syntax lets us create a struct by specifying a struct to base the rest of the values on (after explicitly setting some values). This syntax will move or copy individual fields to the new struct, so some fields of the old struct may become unusable.
#![allow(unused)]
fn main() {
let another_struct = Some_Struct {
field1 : val1
..old_struct
};
}
Tuple structs, e.g. struct Point(i32, i32) are like structs without names for fields.
They let you define distinct types, but give you the dot operator and indexing of tuples.
struct Color(u8, u8, u8, u8);
let color = Color(0,1,2,3);
Order matters and they can be destructured, like tuples. Field names are not provided. Dot access operator is also valid.
Unit-like structs are structs with no fields. They can simply serve as a basis to implement traits on.
struct UnitStruct;
let unit_struct = UnitStruct;
These are probably good for defining traits on.
Storing references in a struct requires the use of lifetimes.
println!(structure:?) is permissible if the structure implements Debug.
println!(structure:#?) for pretty-print is also permissible with Debug.
These are different than `dbg!(…)``.
This will instead print to stderr, and it takes and returns ownership, rather than borrowing.
Method Syntax
Similar to Python’s fucky OOP
basically moving functions into a struct namespace creates a method, but the
first param must be a &self (mutable or not). This is shorthand for
self : &Self which is shorthand for whatever self actually is in the impl.
struct SomeStruct ...
...
impl SomeStruct {
fn some_function(&self) {...}
}
Automatic referencing and dereferencing - all arrow operators or other notions just get wrapped up in a simple dot operator.
In methods &self is shorthand for self : &Self.
In the receiver type, you can do any of:
- an immutable borrow (reading)
- a mutable borrow (mutating)
- or taking ownership (consuming)
Taking ownership is rare and more for a special circumstance of preventing the method caller from using the original structure afterward.
A rust convention for getters is to simply use the field name. This makes it read-only and provides a space for manipulation before returning the private value.
automatic referencing and dereferencing is possible because the receiver type is explicit.
(I.e. there is no -> operator like in C++, only ..)
All methods are associated functions.
An associated function without a self is like a static/class method.
You use :: on the type to invoke it.
Specifying no self parameter in an impl block, like Python creates a sort of
static method on the structure. (Rust uses the word namespace for both this,
accessed by ImplName:: and for namespaces proper from modules.)
6 Enums and Pattern Matching
Enums can be like C-style enums, but they can be more like Java record types. You can have individual, static instantiations/variants imbued with different data.
Variants’ data can even be of different types. These can even be structs or other enums.
pub enum Thing {
Variant1,
Variant2{named_field: i32},
Variant3(i32)
}
Enums can have methods. Enums can be generic.
Option<T> is how the concept of null is implemented.
None is the equivalent variant.
The Match Control Flow Construct
In match expressions, enums support value binding.
Matches are exhaustive.
The catch-all (default) case just uses a variable rather than a value as the match arm.
Using _ is the same, but the compiler will not issue a warning if the value is not used.
To do nothing with a branch, use the unit value.
_ => ()
Concise Control Flow with if let
if let is basically a degenerate form of a match.
It avoids some boilerplate for contriving an equivalent match expression.
You match on one variant and may perform variable binding.
An else in an if let is equivalent to a _ in a match block.
7 Managing Growing Projects
In growth order, Modules -> Files -> Crates -> Packages -> Workspaces
Crates are a rust construct, while packages are a cargo construct. Workspaces are a development time grouping of interrelated packages that evolve together.
Packages must have at least one crate. Only one can be a library crate.
src/main.rs and src/lib.rs are special names that will be detected as the crate root for Cargo.
Then the build result will use the name given in the package manifest.
src/bin can have other executable crates.
Defining Modules to Control Scope and Privacy
Basically mod declares a submodule.
pub on a module and on its members allows you to use those members in the context where you “include” it with mod.
Otherwise, it is still visible to sibling and child modules.
Modules
These introduce namespaces, are related to crates and allow access
specification. (Rust calls it “Privacy Boundary”).
mod some_module {
fn some_function(){
...
}
}
pub some_public_function () {
crate::some_module::some_function() // absolute path
some_module::some_function() // relative path
}
For privacy boundaries, modules, structs, funtions, etc are all private. A child item can access its parent’s fields (so siblings can access each other) however a parent cannot access its child (nested) private fields.
To make something public, use pub before the identifier.
super can be used to access the immediate parent scope.
Structs need a specifier on each field. Enums only need one at the top-level identifier, and all its variants a public by default.
std:: actually specifies an absolute path to the standard library. It is not specified in [dependencies] however because it “ships with Rust.”
There is a top-level crate (with the same semantics as a module) named crate. From it, absolute paths to other modules within the crate can be accessed.
crate::SomeModule::
Use
Bringing in the parent module and referring the target field with the parent’s namespace is idiomatic for functions. For structs and enums, it is idiomatic to refer to the entire path
use std::io; is an example of including packages.
use rand::Rng; is an example of using traits.
use std::cmp::Ordering; for an enumeration
as is used in the same way as Python to rename something included.
pub use allows reexporting or making something that is private and nested some levels in the inner scope to be public to the outter/world.
Nested paths work with use.
use std
use std::io
use std::cmp
// OR
use std::{self,io,cmp} // Self works here too!
Glob Operator
This works for including things. It is not advised but convenient for writing test code, or for this “prelude pattern.”
use std::collections::*
File Separation
use <PKG>; with a semi-colon rather than block means loading defs from a file with the same name as the package.
8 Common Collections
Vectors
v : Vec<i32> = Vec::new(); // declaration by function
v = vec![1, 2, 3]; // declaration by macro
v.push(4)
let elem : &i32 = &v[2]; // gets reference, good for crashing in error handling
let elem2 = v.get(3); // gets Option<&T>
Iterating over immutable references, then mutable
```Rust
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
Indexing with [] can cause a panic.
.get() returns an Option.
You can iterate over a vector mutably or immutably. Regardless, you cannot mutate the vector reference itself at that point.
Wrapping different types in enum variants lets you make a vector of one enum type. Then you essentially have a vector of different types.
Strings
A string slice is str.
Usually it is borrowed as &str.
The data of the string is stored elsewhere.
String literals are stored in the program binary. They are one form of string slices.
Concatenate with +, format!(), .push_str(), and for single chars, .push().
+ looks weird, because the first argument is the String itself (consumes).
The second is an &str.
When adding multiple, it looks weird, so format!() is a good idea.
Indexing into string slices requires a range.
string[0..2]
This is because indexing could represent bytes, characters, or graphemes.
This is dangerous though, and could cause a panic at runtime.
.chars() and .bytes() are useful methods for iteration.
.split_whitespace() is a solid method.
HashMaps
HashMaps are not in the prelude for the core language, but they can be included from the standard library. They also cover the idea of dictionaries/associative arrays in Rust.
use std::collections::HashMap;
One way to generate a HashMap is to create iterators from vectors with .to_iter() and then calling .zip(...) to create tuple pairs, and finally .collect() to return to an annotated HashMap<_,_>
.get() returns an Option<T> as usual.
.insert() for new values or for overridding
.entry() returns an Entry, from which or_insert() may be called to
conditionally insert (only new values), and a ref to the value is returned.
.split_whitespace()
There’s a sort of “structured binding” for key-value pairs.
for (key, value) in &scores {
println!("{}: {}", key, value);
}
HashMaps may either own their keys and values, or they may have references to them.
entry(...).or_insert() is a decently powerful idiom for HashMaps.
9 Error Handling
Functions, many from the standard library, return a Result enumeration. These
are generic and specialized for different modules, like std::io.
These errors must be handled (???: before end of scope or their lifetimes?).
.unwrap() may be used as a more convenient alternative to a match expression to bind the success value or call panic!() on failure.
Result also contains an expect method to add error handling throughout the code. This is like unwrap but one can provide the message to panic.
The ? operator allows error propagation by either returning the wrapped Ok value or by returning the entire wrapping of the Err value. This uses a from() function to convert the returned Err to the calling functions return Err.
These can be chained as in File::open("hello.txt")?.read_to_string(&mut s)?;
std::ops::Try is the trait behind Option and Result.
fn main() -> Result<(), Box<dyn Error>> is main’s other valid return type.
Result
Result<T, E>
unwrap_or_else() allows a closure for not sunny cases.
unwrap() may simply panic.
expect() is similar but you provide a message.
? is the error propagation operator.
The function must have a Result return type.
The operator also adds a From implementation to the error to convert it to the return type.
This may also be used with Option where the early return is None. These cannot be mixed and matched with Result though.
Anything that implements FromResidual can actually be used.
Main may return Result<(), Box<dyn Error>. The latter is a trait object, but that is not explained here.
Main may return anything that implements std::process::Termination.
That gives a report function that returns an ExitCode.
Panic is good for tests, prototypes, or examples. It is also good when the caller has no way of course correction. For example, only having bad input to supply to a function. This is a contract violation.
Basically use Result if you can recover.
Panic!
The panic!("Message"); macro causes Rust to either unwind the stack and exit
or to abort and leave the OS to clean up the memory.
To allow aborting, in Cargo.toml append to the appropriate profile.
[profile.release]
panic = 'abort'
RUST_BACKTRACE is an environment variable which if set to 1 (or further to
‘FULL’) will give backtraces at panicking.
10 Generics, Types, Traits, and Lifetimes
Generics Data Types
Use generics when you find that your code only differs by types (think a payload sender generic over two different parts of an application with different types).
Also, use generics if you are expressing logic of how some types relate to each other without using the type details. Say, collections.
Traits may be used as basically concepts/constraints, called trait bounds.
<T: std::cmp::PartialOrd>
Type parameters may be applied to structs.
#![allow(unused)]
fn main() {
struct Point<X, Y>{
x: X,
y: Y,
}
}
Type parameters may be added to implementations.
impl<T> Something<T>
Specifying the type parameter the first time ties it to the second specification. This lets the compiler know that this isn’t a simple implementation for a concrete type T.
I.e.
This disambiguates specialization of concrete types
impl Something<f32>
At the same time, methods within the impl can have their own separate type parameter.
#![allow(unused)]
fn main() {
fn mixup<X2, Y2>(other: Point<X2, Y2>) -> Point<X1, Y2> {
Point<X1, Y2> {
x: self.x,
y: other.y
}
}
}
Enums may be generic. Option <T> has one parameter and Result<T, E> has two.
Monomorphization is Rust’s name for type erasure or name mangling.
Traits: Defining Shared Behavior
They are similar to interfaces.
Traits are functionality shareable with other types.
Trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish some purpose.
The orphan rule as part of coherence means to implement a trait on a type, the trait or the type must be local.
Traits may provide default implementations in their definition.
This lends to the template method pattern. A default implementation may call other methods that have no default. Thus only those need to be implemented to give the higher level method.
An overriding trait method cannot call the default method.
impl Trait syntax is basically taking a trait as a function parameter.
It is syntactic sugar for trait bound syntax which is the same but shows the trait as a type parameter.
Compare these. The latter restricts types to be the same.
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
And
pub fn notify<T: Summary>(item1: &T, item2: &T) {
Multiple trait bounds can be expressed with +.
Where clauses are nice.
You can return an impl Trait. The caller will only see the trait object.
Blanket implementation is implementing a trait on anything that implements another trait.
#![allow(unused)]
fn main() {
impl<T: Display> ToString for T {
// --snip--
}
}
Lifetimes
Lifetime parameters only make sense on references.
It seems the generic lifetime parameters just link formal parameters with the return type. These are input lifetimes and output lifetime for functions.
There are lifetime elision rules that are not quite full lifetime inference.
- Every input parameter gets an input lifetime.
- If there is only one input lifetime, it is assigned to the output lifetime. (Having an output lifetime unrelated to input doesn’t make sense, because independently created references would become dangling)
- If
selfis present, it’s lifetime is assigned to the output lifetime (makes methods cleaner often).
11 Testing
Using attributes enables unit (and integration) tests. These are the #[] above some definitions.
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
cargo test can be used to build the test module and run all unit tests in
separate threads.
assert_eq!(), assert_ne!(), assert!(), are useful macros for unit tests. The
first two provide a bit more detail to cargo by default.
Any string after the arguments is passed to format!() for more detailed.
#[should_panic] can be attributed after a #[test] and can even be made to
match on specific panic messages with a parameter.
#[test]
#[should_panic(expected = "some substring")]
Result<T,E> can also be returned by tests (so, Ok(value) for success or
Err(message) for failure. This allows the ? to be used.
#[ignored] is useful.
#[cfg(test)] is on the module.
#[test] is on the function.
assert!(), assert_eq!(), and assert_ne!() macros are available.
The latter two require their operands to have implemented both PartialEq and Debug for comparison and printing respectively.
#[should_panic] and #[should_panic, expected="..."] are useful.
Tests can also not be based on panicking.
Then they return Result<T, E> (e.g. Result<(), String>).
To check for an Err variant, use assert!(value.is_err()).
Tests run in parallel by default. This means if a test shares resources like a file, either copies of resources must be made, or tests must be run serially, or I guess some synchronization could be done.
Flags can be passed to: show success output, filter by name (including test module name), and run ignored tests.
#[ignore] goes after the test macro.
Unit Tests
#[cfg(test)] means to only compile the module when the configuration
attribute includes the test parameter (this is provided by cargo test).
Integration Tests
For Cargo under tests/*.rs all files will be compiled as separate test crates
; however, source files under subfolders will not be (so common code may be
extracted).
Modules here do not need the configuration attribute, since the folder path implies compilation specifically for testing.
Binary crates do not have integration tests, only library crates.
Unit test convention is to have the #[#cfg(test)] module in the source file alongside the code. This macro will only have the test code built with cargo test. This makes use super::*; common.
Integration tests are specifically for the public API and use a tests/ directory.
Each file is a separate crate representing a separate integration.
cargo test --test <file> will target only that integration.
Subdirectories under tests/ do not get handled as files directly under it do. This may be used to provide common code to share between integration tests.
Integration tests are only for library crates src/lib.rs. However, a binary crate that factors out its functionality may then use them.
Automated Tests
cargo test -- --test-threads=1 can be used to run tests serially (no
parallelism).
cargo test <PATTERN> will match test functions names on a pattern. Test
modules become a part of this pattern.
cargo test -- --ignored runs the ignored tests only.
12 Command Line Program
The configuration structure and run function are broken out into library code.
The run function returns a Result<(), Box<dyn Error>> which can be propagated.
That is handled with a process exit code in main.
Environment variables are accessed with env::var.
CLI arguments are accessed with env::args().
It also uses std::process for exit codes.
std::env::var
Printing to standard error may be done with eprintln!().
13 Functional Features
Closures
Closures are basically lambdas.
let foo = || x + 2;
The syntax can be annotated or pretty reduced.
Closures pass arguments in the same three ways as functions.
Explicitly taking ownership (moving/capturing) can be done with move.
Functions that take closures may have their parameters trait bound.
There are three Fn traits.
FnOnce means the closure gives back ownership of something it captured.
The means it cannot be called again.
FnMut mutates something it captures.
It can be called more than once.
Fn does neither, so it can be called more than once and concurrently.
Iterators
Iterators are used for functional programming style and allow the abstraction of often more mundane iteration tasks (such as on CLI arguments).
zero-cost abstraction coined by Stroustrup. This sort of means that lower-level code that is hand-written is subject to inoptimization and higher-level abstractions may be more optimized by the compiler (nothing is lost by using higher-level abstractions).
The iterator trait is pretty simple.
It has an associated type though.
That has its own syntax.
iter(), iter_mut(), into_iter() provide the three forms of getting values. The first is an immutable reference and the last is ownership.
There are consuming adapters like .sum() and .collect().
These use up the iterator and produce a result based on all the items.
There are iterator adapters which produce a new iterator by changing the original.
An example of this is .map() which takes a closure, applied for each item.
Then there is a good demonstration of improving the handling of env::args (an iterator) by directly using that rather than collecting it into a vec.
Also there is an example of, instead of declaring a temporary buffer, looping through lines, and filtering by assigning to the buffer, a simple iterator adapter .filter() is used. This shows that iterators are basically loops as objects which can have operations applied to them.
14 Cargo
crates.io Existing repos can be easily translated to cargo repos.
cargo new <PKG> or cargo new --lib <PKG>
In the package… cargo build cargo build –release cargo run cargo check cargo update cargo doc –open # builds and presents all dependency docs
Specifying dependencies uses SemVer.
[dependencies]
a_pkg = "3.4.1"
A bare version number is shorthand for “^3.4.1” which really matches any version that is API-compatible (so, up to the next minor release).
Locking
Cargo.lock contains version numbers of dependencies where the last stable build
was made. These will not update unless cargo update is run.
Even with an update, the dependency will not roll to the next minor release unless the Cargo.toml file is actually changed.
Crates
Packages (the source repo started by Cargo) can either contain (or produce, in a sense) either binary crates or a single library crate.
Implicit Crate
There is a top-level crate (with the same semantics as a module) named crate.
From it, absolute paths to other modules within the crate can be accessed.
crate::SomeModule::
Automated Tests
cargo test -- --test-threads=1 can be used to run tests serially (no
parallelism).
cargo test <PATTERN> will match test functions names on a pattern. Test
modules become a part of this pattern.
cargo test -- --ignored runs the ignored tests only.
Release profiles
These are different configuration options in the TOML file for different kinds of releases of the software.
[profile.dev], [profile.release], etc.
Packaging
The [package] tag is used. Under this is specified common data with which to
upload a software package to crates.io.
cargo login uses ~/.cargo/credentials to store the private API token.
???: Is there a way to spin up a common registry for Rust crates? There must be.
cargo yank --vers x.y.z locks prevents future projects from using your software version
within their Cargo.lock file. It does not delete code.
cargo yank --vers x.y.z --undo undoes this (unsurprisingly).
Workspaces
a set of packages that share the same Cargo.lock and output directory.
├── Cargo.lock
├── Cargo.toml
├── add-one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
Different versions of dependencies within different TOML files will be resolved and will be decided within the top-level Cargo.lock file.
Dependency paths must be added in one crate’s toml file to another crate in
order to use that crate. use statements must still be used.
cargo run -p <CRATE> allows one to run a specific crate in the workspace.
cargo test -p <CRATE>
cargo install can be used to additionally install extensions to Cargo itself,
so called cargo-<NAME>. These can then be run as cargo <SUBCOMMAND>.
Cargo packages are intended for developers, not system operation. (apparently)
15 Smart Pointers
Smart pointers own their data, unlike references.
Smart pointers are structs that implement Deref and Drop
StringandVec<T>Box<T>- heap allocationRc<T>- reference countingRef<T>,RefMut<T>,RefCell<T>enforce borrowing rules at runtime rather than at compile time.
Standard pointers in Rust are like references but can own their data.
interior mutability pattern
Box<T> is a for simple heap allocation.
It is useful in, say, creating recursive types.
A box in the standard library is basically a tuple of one element.
This element implements Deref, so that the * operator may be called on it.
This operator first gets a reference to the inner element (in the tuple).
This would seem backwards.
Then it de-references that to get and return the inner value.
Implicit Deref Coercions
Deref coercion is similar to automatic unboxing. It can do more though (or auto-unboxing is one type).
Passing references or pointers (such as Box<> values) may be automatically, implicitly dereferenced.
It seems harry but makes the code look cleaner.
For example, Box<String> can be passed to &str and coerced to that type.
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
Without deref coercion, you would need to write:
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
There is also DerefMut.
Per the book,
Rust does deref coercion when it finds types and trait implementations in three cases:
- From
&Tto&UwhenT: Deref<Target=U>- From
&mut Tto&mut UwhenT: DerefMut<Target=U>- From
&mut Tto&UwhenT: Deref<Target=U>
Drop
Drop is the other trait included in the prelude to implement for a smart pointer. This executes right as the pointer goes out of scope.
impl Drop for CustomSmartPointer {
fn drop(&mut self) { ... }}
It cannot be called manually if destruction is desired before end of scope.
Instead, std::mem::drop() must be called.
(Otherwise this would cause a double free error).
Reference Counting
std::rc::Rc is not brought into scope by the prelude.
This allows multiple references to the same object (which will not be destructed until the final reference goes out of scope).
Declare a Rc<T> rather than Box<T> if ownership must be shared.
Rc is for single-threaded applications only.
Rc only allows immutable borrows.
For example, two pointers in two different objects point to the same data.
Rc::clone(&some_rc) is convention over some_rc.clone() because it is unlike other cloning methods.
Rather than make a deep-copy, it just bumps the reference count.
The count may be gotten with Rc::strong_count(&rc).
Interior Mutability
This can be done with RefCel<T>.
RefCell<> moves borrow checking to runtime.
This is useful for example for creating mock objects which must conform to immutable signatures but hold mutable data.
(See the explanation below.)
Cell<> does this for copying data and Mutex<> for thread-safe scenarios.
It’s weird because it looks like it defeats the purpose of immutability.
I think the idea is that the RefCel itself, the pointer, is constant but can point to mutable data.
This seems to be the canonical construct in rust with this power, called interior mutability.
Violating the borrow checker’s rules with this will cause a runtime panic, not a compilation error.
The example is a long one, showing mocking for testing. Instead of state verification, it shows that the methods called on the collaborator objects by the method under test can be wired to verify the object under test.
This can be chained with Rc to get a shared pointer to mutable data, i.e. Rc<RefCel<T>>.
Calling .borrow() returns an immutable pointer, specifically a Ref<T>.
Calling .borrow_mut() returns a mutable pointer, specifically a RefMut<T>.
Mutex<T> is the thread-safe version of RefCel<T>.
Using Rc<T> can create reference cycles, like a cycle in a linked list.
When the two original pointers to the two data are dropped, the internal linkages between them remain (accounted for in the reference counting).
This means the memory would never be unallocated.
A way to avoid this is with Weak<T>, which is a type related to Rc<T>.
These are almost like symlinks v. hard links.
They increase the .weak_count() but not .strong_count().
So if a weak link remains but the strong count hits zero, the memory is still unallocated.
It is the same otherwise, but semantically it doesn’t express ownership.
For example, in a tree, the parent has an Rc<T> to children nodes.
The children nodes have a Weak<T> to their parent.
To use a Weak<T>, you have to weak.borrow().upgrade() and check the Option<T>.
Or Rc::downgrade(&strong)
16 Fearless Concurrency
handle : JoinHandle = thread::spawn(|| {})
Can move captures into the thread.
handle.join().unwrap()
thread::sleep(time::Duration::from_secs(1))
Also, to avoid move into threads.
thread::scope(|s| {
s.spawn(|| {
println!("Here's a vector: {v:?}");
});
});
Messaging Passing
use std::sync::mpsc
let (rx, tx) = mpsc::channel();
You can give a transmitter to another thread. Threads must own their transmitter (or I guess receiver).
Sending data moves it to the other thread.
Sending returns a Result<T, E> so you can check if the channel already closed.
You can call recv on the receiver, which blocks but will return a Result<T, E>.
An error will be returned when thecloses.
try_recv will return immediately, either with Some(T) or an Err if there are no messages.
You can also clone transmitters, fulfilling the name multiple producer single consumer (MPSC). So, multiple threads can have their own producer/transmitter.
Shared Memory
Messaging passing is the equivalent of a single owner of data. Shared memory is like having multiple owners.
This can be done with a std::sync::Mutex.
A good metaphor is acquiring a microphone in a panel in order to speak among a crowd.
Mutex::new(...) returns a Mutex<T>.
Calling .lock() will block and return a LockResult.
It will be an Err variant if another thread with the lock panicked.
Otherwise it will be a MutexGuard.
That is a smart pointer which can be dereferenced to access/modify the value you care about.
It will also unlock the mutex on its Drop implementation.
The problem with Mutexes (Mutices?) is that you need a shared reference among threads to make them useful.
Thus you must wrap them into a reference counting construct like Rc<T>.
Rc<T> is not thread-safe, however, but Arc<T> is.
This is an atomic Rc.
There are other atomic primitives.
Using Arc<Mutex<T>> is analgous to Rc<RefCel<T>> in single-threaded world.
This is because Mutex<T>, like RefCel<T> actually provides interior mutability.
Mutexes can lead to deadlocks.
Send and Sync
Both std::marker::{Send,Sync} are “marker traits”.
All they do is mark the thing that implements it.
If something implements Send, it is safe to move to another thread.
Rc<T> cannot implement this, for example, because sending a .clone() could have logical rammifications (both clones update the same count).
If it implements Sync, immutable references to it can be sent to another thread.
(If &T: Send then T: Sync.)
Implementing these yourself is part of unsafe rust. A type built only of types that have these markers will then have these markers themselves.
17 Fundamentals of Asynchronous Programming: Async, Await, Futures, and Streams
There is a trait Future, which is the backbone of asynchronous Rust.
async marks functions as asynchronous. They can be interrupted and resumed.
Calling .await on a future is a time (an “await point”) for the async function to either continue immediately or be interrupted.
(This lets the async runtime know it is a good time.)
This is because a future is expected to take some amount of time, so it may make sense to .await it.
async functions actually compile to regular functions that return an anonymous implementation of Future.
In other words,
async fn foo() -> bool {
true
}
becomes
fn foo() -> impl Future<Output = bool> {
async {
true
}
}
So there are just async and async move blocks.
An async runtime is basically a statemachine with an executor which is the side that executes the statemachine.
select! can be used to await multiple async functions.
Async becomes concurrent when wrapping in tasks. You can join on tasks much like threads. You can use an async runtine’s (separate) join function to join multiple task handles. This will wait until all are complete.
Async messages/channels are similar to mpsc.
However the receiver is mutable.
rx.recv() also produces a Future.
You can receive an unknown number of values with while let.
while let Some(value) = rx.recv().await { println!("received '{value}'"); }
However, the transmitter must close to end this loop. It will only close at the end of the transmitting task if the transmitting task owns the transmitter. For this, you need to async move.
join() has related methods like join3() to add more Futures.
join!(...) can take any known number of Futures
join_all(...) can take an iterator (unknown) of Futures (but they must have the same <Output = ...> type).
It is a tradeoff.
To create an iterator of Futures, you use a trait object.
This is similar to Box::new(dyn Future<Output = ...>) but you must pin it.
Box::pin(my_future).
You don’t actually need the heap allocation, just the pin, so you can use a Pin<&mut dyn Future<Output = ...>> type.
You can construct that with pin!(async {...}).
You can race() futures like joining then, but only one completes.
You can also yield_now() to introduce an await point at any time.
18 OOP
Rust supports OOP in that it:
- provides objects and methods via structs, enums, and implementations.
- provides encapsulation with
pub - provides limited inheritance with default trait implementations
- provides bounded parametric polymorphism using generics and trait bounds
Trait objects allow dynamic dispatch, whereas generics allow static dispatch. Dynamic dispatch has a runtime penalty, but it makes the code more re-usable.
Dynamic dispatch is done with dyn.
Basically you use a pointer to a trait object.
The indirection is necessary, because you do not know the size of the backing implementation.
field : Box<dyn T>
State Pattern
There is then an example of the state pattern, which is one of the GoF patterns (I think). This has a wrapper object that holds a state value. That state value is really a pointer to a trait object. The trait has methods that define the state’s transition to other states. Thus when any of those methods are called, the current state may transition to another state per its encapsulated implementation. The wrapper object also implements the trait and just forwards it to its current state.
The pointer to the trait is also wrapped with Option.
This is necessary during state transition for the rust ownership system.
This is so the state can be .take(), replaced with none, and then the value can transitioned and reassigned to the pointer of the wrapper object.
Then the chapter shows that this is all not as efficient as a more rust-y way.
This way is to just define each trait implementation (the various states) as separate structs.
Those then have methods that consume themselves and construct the next struct.
This is much simpler because, it seems, the consumption semantics of rust are utilized rather than all the .take() nonsense.
19 Patterns and Matching
Patterns are a language construct used in several contexts.
The most obvious is at the beginning of match arms.
Also they are used with if let and while let.
Actually they are used with simple if, like in tuple destructuring.
They are also used with function parameters and closure parameters.
Named variables in a pattern will shadow variables outside the match arm’s scope.
Refutable patterns are those that may fail, because they have some structure that may mismatch the value given.
Irrefutable will always match/bind.
These could be a simple variable or an _.
let requires an irrefutable pattern while if let requires a refutable pattern.
Otherwise each expression would be meaningless.
match allows one branch to have an irrefutable arm.
let else might be a valid construct without an if. Hmm.
Syntax
You can match literals and named variables. Because named variables shadow higher scope variables, you cannot use them as conditionals in the match arm directly. You need a MatchGuard.
A | can join different conditions in a pattern.
You can destructure structs as well. You can also use struct field init shorthand to capture a component in a new, independent variable. You can also match with a field of a structure at a certain value.
The wildcard _ never binds.
A name starting with an _ does (and takes ownership).
However an unused warning will never be issued for of.
The .. syntax is like a wildcard but for a range.
It can even appear in the middle of a tuple.
A match guard is basically just a <pattern> if ... => { }
This allows using outter, non-shadowed variables.
It is an and with the pattern, and the pattern can have or with |.
There is also the @ syntax.
This can be applied to binding in a pattern to also test the value.
The book shows an example of a range.
id : id_variable @ 3..=7
20 Advanced Features
Unsafe
There are five things that unsafe rust gives you.
- dereferencing raw pointers
- calling unsafe functions or methods
- accessing/modifying mutable static variables
- implementing unsafe traits
- accessing fields of a union
Raw Pointers
Raw pointers are similar to C-pointers.
For immutable (*const i32):
let ro = &raw const num
For mutable (*mut i32):
let rw = &raw mut num
With a numeric address (a usize), you can cast it as a raw pointer.
let r1 = some_address as *const i32;
They may be declared anywhere but only dereferenced in an unsafe {} block
Unsafe Functions
Declare unsafe functions as unsafe fn foo(){} which can only be called in an unsafe block.
#![allow(unused)]
fn main() {
unsafe {
unsafe_function();
}
unsafe fn unsafe_function() {
}
}
This seems redundant but is sort of a handshake between the caller and the subroutine to help minimize the scope of the unsafe function. I guess this is similar to minimizing scopes or error handling and async functions. Break up scope as much as you can.
Their example is sort of trivial, but they show using an assert to first confirm some data is good (splittable at some index), and then using an unsafe std library function to split an array and get two immutable references at once.
Extern Blocks
Using an extern block to declare foreign functions (like prototypes/signatures) is marked unsafe.
#![allow(unused)]
fn main() {
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
}
Here “C” isn’t really just the language, it’s C’s ABI. So any language that uses C’s ABI can be linked to this.
Calls to extern blocks (foreign function interface) must be wrapped in unsafe blocks.
But since we know C’s abs function is safe, we can mark it as safe. Then it doesn’t require an unsafe block at the call site.
#![allow(unused)]
fn main() {
unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}
}
Providing a function from Rust callable by foreign languages does require unsafe in the no_mangle attribute. This is because, without the compiler mangling function names, they could collide. So you have to make sure that won’t happen yourself. I guess that relates to monomorphization. Just like async, extern goes after visibility but before fn.
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
Mutable Static
Basically const and static are similar, but static has a fixed memory address. Const is more about the value, not about storing… Therefore unlike const, static can be mutable.
I guess too, const is like constexpr, whereas static is like traditional const.
If a static is made mutable, then accessing/modifying is inherently unsafe.
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
Unsafe Traits and Impls
As with other categories, this is two things. Both marking a trait as unsafe when “at least one if its methods has invariant that the compiler can’t verify.”
#![allow(unused)]
fn main() {
unsafe trait Foo {
// methods
}
}
And marking unsafe implementations, such as implementing Send and Sync for a struct containing raw pointers.
#![allow(unused)]
fn main() {
unsafe impl Foo for i32 {
//
}
}
Accessing Fields of a Union
???
Miri
You can add the nightly rustup component miri to do dynamic analysis against unsafe rust code.
Advanced Traits
Associated Types v. Generic Traits
“Associated types let the implementor absorb the decision to determine a type, so that callers don’t have to carry the type everywhere.”
The key insight is: “Associated types also become part of the trait’s contract: implementors of the trait must provide a type to stand in for the associated type placeholder.”
The mental model: use an associated type when the type is an output or consequence of the implementing type (one right answer), and use a generic parameter when the trait should be implementable for many different types simultaneously (like From<T>).
…
Basically this is an alternative to generic traits. The difference is you can only implement traits with associated types once. It semantically indicates a different relationship: one implementor has “one output type”.
It is used specifically to constrain design. That constrain buys you things. It allows the compiler to infer types in places. It makes trait bounds simpler/cleaner.
Better yet, it also removes the burden of the caller to determine/specify type parameters at call sites, because they are already fully known.
For example, Iter has an associated type Item. The one implementing Iter specifies its type. Otherwise, the caller (constructor of the implementor) must also provide the type parameter that makes sense with the iterator.
For example, HashMap<K, V> implements iterator with Item = (K, V). Then map.iter() can be used. With a hypothetical Iter<T>, that “bubbles up” or percolates up to the caller, so they would have to specify HashMap<K, V, I>, but I is already known by the internals of the HashMap.
Technically, you could forward a type parameter from the implement or to the associated type. This would return you to having HashMap<K, V, T> and defeat the purpose of an associated type.
On the contrary, associated types enable GAT, or generic associated types. This let’s you have something advanced, like iterating over items from an implementor whose lifetime is bound to the implementor. Seems… Complex.
Default Generic Type Parameters and Operator Overloading
This has a pretty badass example with using the newtype pattern to create millimeter and meter types around i32, and then implementing add for millimeters. That defaults to adding another millimeters to the RHS, however, another implementation can be used to implement adding meters. The associated type for both is the single, correct output type of millimeters.
Extra about custom literals…
| Technique | Syntax | Notes |
|---|---|---|
| Constructor | Meters::new(5.0) | Most idiomatic |
| Into | 5.0.into() | Requires type annotation |
| Macro | m!(5.0) | Closest to custom literals |
| Extension trait | 5.0_f64.m() | Most fluent/ergonomic |
| Operator overloading | m!(5) + m!(3) | Combine with any |
| The extension trait + operator overloading combo is as close to custom literal ergonomics as you’ll get in Rust today. There’s a long-standing desire for proper custom literals in the community, but it hasn’t made it into the language yet. |
Disambiguating Between Methods with the Same Name
Implementors can implement multiple methods from different traits with the same name, and even their own method of the same name.
Their own, direct method is preferred.
To then specify one of the other methods, you use that reversed syntax similar to String::from, like
#![allow(unused)]
fn main() {
MyTrait::method(&my_obj);
}
Left off at… “However, associated functions that are not”
If you’re “overriding” an associated function from a trait with an implementor’s own method, then you have to use fully qualified syntax to invoke the associated function. So like…
#![allow(unused)]
fn main() {
<Dog as Animal>::baby_name();
}
Supertraits
Basically this is Java’s extend syntax, but on traits. It is syntactic sugar for taking a bound on Self. In other words,
#![allow(unused)]
fn main() {
trait Warrior: Fighter {}
// equivalent to
trait Warrior where Self: Fighter {}
}
Newtype Capability
Newtype let’s you get past the orphan rule. You can implement external traits on external types, because you’re implementing them on the wrapper.
But then, to make something like “embedding” (Go terminology), you would have to implement Deref on the wrapper.
Advanced Types
Newtype Pattern v. Type Aliases
Basically this just says, use newtype to not confuse units and to further minimally hide private APIs/restrict types.
Meanwhile, an actually type alias
Empty/Never Type
The type ! is used when something diverges or never returns. This can be coerced into any type. So like, a match arm that diverges will work with any return type. I guess that also explains let ... else { ... }
Unsized Types
str and trait objects are unsized.
str is because different objects have different lengths, yet for one type, there must be one size.
So, these must be used behind a pointer of any kind. References are typical but others work.
Generics implicitly take a Sized bound, but you can overrideit with
#![allow(unused)]
fn main() {
fn too<T: ?Sized>(t: &T) {
// t can be any pointer.
}
}
t must still be a pointer type despite the trait bound.
The ? is special only for the Sized bound, which means may or may not.
Advanced Functions and Closures
You can take function pointers as arguments.
This is type fn as opposed to traits like trait Fn.
#![allow(unused)]
fn main() {
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg) }
}
Function pointers can be coerced to those traits/closures. They are flexible as arguments, but the reciprocal is true for parameters. Requiring a function pointer as an argument makes you unable to pass closures. So accepting one of the closure traits is best.
The name of enum variants become initializer functions, which become function pointers, which can be coerced into closures. For example:
#![allow(unused)]
fn main() {
enum Color {
RGB(u64),
GrayScale
}
let color_list: Vec<Color> = (0u64..20u64).map(Color::RGB).collect();
}
Returning Closures
You can return a closure as a function pointer if it doesn’t capture anything.
If it does, you can return it with the impl trait syntax.
But if you are trying to call it more than once and return different opaque types, you need to use Box<dyn Fn(param) -> Retval>
Macros
There are declarative macros and 3 procedural macros.
Declarative are the macro_rules!.
Procedural include:
- custom derive on structs and enums
- attribute macro on any type
- function-like macros
Macros can provide function overloading (taking a variable number of arguments anyway).
Macros must be written above where they are called, unlike functions.
Declarative macros are basically match expressions, but against rust code, and they cause code generation in the macro’s place.
#![allow(unused)]
fn main() {
`#[macro_export]`
macro_rules! guh {
( $() ) => { ... };
}
}
The Little Book of Rust Macros
Derive macros have way more steps.
- First you create a crate for your trait.
- Then you need to create a separate, sub-crate (conventionally named with
_derive) . - It must have
proc-macro = truein its Cargo.toml - You use a macro on your derivation function, tale in a TokenStream and return a TokenStream.
- Then you normally use the syn and quote crates to parse and generate rust code respectively.
stringify!keeps expressions unevaluated for the macro.
Attribute macros are very similar, but more flexible.
You use #[proc_macro_attribute] rather than #[process_macro_derive].
Then you write a function that takes two token streams, the first is the attribute and the second whatever is wraps.
Function-like are similar to derive in that they take one token stream. They use #[proc_macro].
Async / Await
Concurrent programming is writing code in such a way that it appears to do multiple things at once.
It is entirely within the program.
It cannot make the program faster, but it can make the program more responsive.
Threads are owned and managed by the OS. One process may have multiple threads. Threads share memory.
Tasks on the other hand are managed by the asynchronous runtime within the program.
The OS preemptively schedules threads to run. It context switches to do so. Tasks, at least with Rust’s tokio runtime are cooperative. They voluntarily yield control.
Green threads typically refer to tasks that are preemptive. Also, tasks are internally structurally different from threads.
Async IO is separate from, but often used with, async concurrency. It is also called “nonblocking IO”.
Instead of a thread blocking and waiting doe the OS while other threads run, the OS does not block the program with the async runtime. The async runtime returns control to a different task. Then when IO results are available, the runtime provides it to the waiting task.
Often programs use both async tasks and threads. Sometimes the async runtime uses multiple threads without them being written into the programming logic. This may be necessary because only threads can execute on multiple cores for parallelism.
Zero processing and synchronization requiring tasks present a challenge for scheduling algorithms.
Concurrency is a pattern controlled by the code and parallelism is a resource provided by the scheduler.
A task group could be an idea of assigning all tasks to one thread locked to one core, so they run concurrently but never in parallel.
Runtimes have a reactor/event loop/driver, scheduler, and _executor/runtime.
Calling await joins the future’s execution to the current async task.
Sleeping a task uses the runtime’s sleep method, not the thread sleep method.
Actually making tasks concurrent requires that the task be spawned, not awaited.
Spawning a task returns a JoinHandle future. Awaiting that future is like joining a thread.
Per John Moon
Style/idiom linting: cargo clippy on project
Building configs into the binary: macro include_str
Command line arguments: clap-rs
Printing structs: #[derive(Debug)] then println!("{:?}", my_struct)
Rocket
The lifecycle is:
- routing
- validation
- processing
- response
Routes have #[get], put, post, delete, head, patch, or options.
There are two kinds of entrypoints.
#[launch] and #[rocket::main]
Head requests are handled automatically if there is only a get route (no explicit head route).
Rocket will reinterpret web forms submitted by post as a different HTTP method if the content type and form’s first field fulfill some requirements.
This gets around web browsers’ only supporting forms with get and post.
Dynamic paths use <variable> in the path of the route.
A segment is one part of a route between slashes.
These dynamic segments bind to function parameters.
Any type implementing FromParam can be used as the parameter.
These are parameter guards.
With segment guards, a parameter may bind to the rest of a path.
In the path, there is <path..>.
The parameter must implement FromSegments.
There is an example of serving a static file with a route. There is an example of using a one line file server with no route (but still a mount point).
Ignore segments with <_> and <_..>.
Rocket forwards failed parameter bindings to the next route. A failure on the final forwarding guard triggers the error catcher.
You can bind to an Option<> or Result<> and not deal with forwarding.
The rank parameter in a route may be necessary to resolve collisions.
Automatically assigned ranks have negative values (higher precedence). Basically the more specific a route and more specific a query, the higher the precedence. This uses a system of “colors”.
- static
- partial
- wild
- none
Exam
What inspires you to learn something new?
For me, there are several motivating factors: novelty, effectiveness and efficiency, helpfulness, and competitiveness. Novelty is the simplest, intrinsic motivation, or as a coworker termed it, “chasing the shiny”. I take a simple joy in discovering new ways to think about problems or finding elegant solutions. For example, at my most recent workplace, we switched from a pattern of using GNU make to using a new tool Earthly. The new tool was highly compatible with Docker (in a CI/CD or build context) but also provided the DAG semantics that GNU make provides.
There is also the matter of effectiveness at solving the problem at hand. That is, I want to learn the new thing that is “the tool for the job”. For example, on a recent project, I had to write a micro-service to integrate with gRPC, Helm, and NATS. Many on our team had opted for Java or Python for their services, but only Golang had native integrations for all three. I dove into it eagerly. As it turned out, Go was also more efficient, with a much faster turn-around for prototypes, compared to getting Java (and Maven) working in our environment.
I am also motivated by becoming more efficient. In terms of the “1% rule”, if I become 1% more efficient each day, the compound effect is enormous. For example, integrating AI into my workflow with Ollama and Continue has certainly granted this and more.
Similarly, remaining competitive with the market is a huge motivation for me to learn new things. For example, Rust and Go are two languages that are “hotter” on the market, especially with the Department of Defense aiming to replace legacy C++ systems with Rust, and the Linux kernel starting new modules in Rust instead of C. Aligning my own projects on my own time is one way I do this. For example, I have been switching my personal website from the static site generator Cobalt (written in rust) to Rocket (an actual rust library, similar to Python’s flask) plus MdBook (the rust library that the rust documentation is processed with).
Lastly, learning new things lets me be more helpful in the workplace. I had the privilege of working for several years with a “10x developer” (or so we liked to refer to him as), who always seemed to have a solution when I was spinning my wheels. It was very inspiring for me to become the same in my professional growth. Although I can’t dive into every technology, I can try to constantly add tools to my toolbox and increase my vocabulary to navigate new problems when they arise.
What’s one professional achievement you are particularly proud of, and why?
<p>I am particularly proud of my work on one software "candidate" (or "epic" in agile methodology terms) called the "diamond" because of its deadline, scope, and my execution of it. The diamond was a "symbology" (or symbol) drawn on the HUD (heads-up display) of the A-10 aircraft. Anything in the pilot's line of sight that fell within the diamond was defined to be in range of the heat-seeking laser rockets. The diamond had been designed by some systems engineers but no implementation was started yet.</p><p><br></p><p>In terms of deadline, the diamond had been a "want-to-have" candidate that had been pushed out several times. We were nearing the end of our software release cycle, but I felt confident that with enough effort and focus, I could complete it. I volunteered to take it on. The estimate of time it would take has been one of my most accurate in my professional career, likely because I had had great exposure to the different subsystems of the A-10. On the last long day of the implementation phase, right before the "code freeze", I had my last successful test on the high fidelity test stand.</p><p><br></p><p>In terms of scope, the diamond touched three major subsystems of the aircraft. While it was displayed alone on the HUD through vector graphics (and an assembly language), command-and-control of the drawing hardware was done through another system. That system also plumbed values used for the diamond by yet another system. The data flow was complex, but fortunately, I had previously volunteered to work all of the various subsystems that it required, including legacy ones.</p><p><br></p><p>Lastly, in terms of my execution, it was a rare software problem that felt like "pure development". The effort was not wasted in monotonous tasking, such as for information foraging or software approvals. Once I had the data properly plumbed, it was a joy of algorithm design and mathematical calculation. I determined the correct formulas for drawing the vectors based on the input and translating that to the assembly language of the HUD.</p>
What new skill are you interested in learning?
<p>There are several new skills I am interested in learning, ultimately to center my technical stack and increase my knowledge of the domain. To center my technical stack, these include rust libraries focused on command-and-control, tooling for behavior-driven development and software architecture. To increase my knowledge of the domain, this includes things like launch stages, mission assurance, space observability, signal processing, etc.</p>
Please share how you resolved a conflict within your team.
<p>Our team had a long running issue of what versioning strategy should be used in a new system-of-systems. Many of the components we had inherited from other teams and times, and at least one was from an external party. This meant there were many different versioning strategies, such as SemVer or simply raw dates. Sometimes source repositories would use things like git submodules, or during build time, they would pull in dependencies from other projects.</p><p><br></p><p>We had a meeting about this, but there was not a clear agenda, and it was unclear who would take responsibility for making the final decision. Many of the positions in the meeting had overlap, but ultimately, no decision was made. An email chain begun by a grumpy systems engineer asked what progress was made and when would a ruling be made.</p><p><br></p><p>I captured the positions of the team members on how to do the versioning, and I explained in-depth how my solution would cover their needs. In particular, the boundaries of a source repository should not be determined by semantics (or what seems "sensible"), or by how big the repository is, or otherwise. Instead it should be handled by what can be versioned independently. I then broke out all of the components in the new system-of-systems, and how we might proceed implementing it. At the end of my explanation, I asked if this solution was sufficient, or if not, what particular shortcomings it had. Ultimately, the team lead agreed with my position and championed it, so it was implemented.</p>
What does the await operator do in Rust?
<p>Conceptually, it adds the execution of the future it is called on (some asynchronous task) to the current asynchronous task. If that asynchronous task can complete without blocking, it does so and returns immediately. If it must block, the async runtime may switch to another task.</p>