Resources
Getting Started with a Rust CLI
Getting Started with a Rust CLI
Project layout
https://doc.rust-lang.org/cargo/guide/project-layout.html
Interesting Syntax
They have an end-inclusive range syntax:
fn main() {
let x = 1..4 // 1, 2, 3
let y = 1..=4 // 1, 2, 3, 4
}
Mutable references to self (TODO / incomplete)
The .sort
function for integers has the following signature: pub fn sort(&mut self)
. This seems to mean that you can only call this function on a mutable reference. I haven't fully figured out how this work.
let vec = vec![3, 1, 2];
let mut_ref = &mut vec;
mut_ref.sort();
Turbofish
https://matematikaadit.github.io/posts/rust-turbofish.html
The turbofish is used in expression contexts to concretize the generic. The first time I used it was to .collect
an iterator into a HashSet
, .collect::<HashSet<Vec<u64>>>()
. When sub-types do not matter you can leave them out with .collect::<HashSet<_>>()
.
Borrowing
Borrowing is the umbrella word for the cleverness within Rust that lets it maintain safety and performance. Borrowing is intended to suggest an idea of ownership, and that the borrower does not own what they've borrowed. There's only ever one owner and when the owner has completed execution the values it holds are destroyed. Other functions / scopes (including nested scopes like for-loops) can be given ownership and after they are done the owned value is destroyed. They can also borrow / reference a value and after they are done the originating function retains the value.
{
let nums = vec![1, 2, 3];
for n in nums {
// ownership has been passed into here
// and in here it will die
}
}
{
let nums = vec![1, 2, 3];
for &n in nums {
// n has been borrowed
// after this you can still reference n
// outside of here, where ownership
// has been maintained
}
nums;
}
{
let mut nums = vec![1, 2, 3];
for n in &mut nums {
// n has been borrowed
// `&` can be thought of as a reference to a value
// `*` can then be thought of as a "dereference" of
// that value. So we are dereferencing `n` and
// modifying it.
*n += 1;
}
// you can later access it because it was only borrowed
// aka, referenced.
nums; // vec![2, 3, 4]
}
Lifetimes of borrowed values
One problem with borrowing is when the borrower is at risk of outliving the borrowee. If the borrowee finishes executing then the value should be destroyed. But if the borrower is still borrowing it then there is a problem.
The below function is trying to let x
get borrowed while also immediately afterwards ending, meaning that x
would get immediately destroyed
fn bad() -> &i32 {
let x = 5;
&x
}
Common function behaviors are (1) to work only with borrowed values fn good(n: &i32) -> &i32
, or (2) to generate data and fully pass ownership fn good() -> i32
.
Strings
Interpolating strings
If you want to interpolate a cli output println!("{}", "hello")
. If you want to compose a string and interpolate some values you can use a combinations of String::push_str
and the format!
macro.
let s = String::from("starting");
s.push_str("continuing");
let end = format!("the end{}", "!");
s.push_str(end);
String
and str
StackOverflow answer on the difference between String
and &str
Think of String
s like Vec
s. It's a dynamic heap string type.
Derived traits
Debug
is an important derived trait for structs. There are a few others as well. You add a derived trait via the derive
attribute.
#[derive(Debug, Clone)]
struct User {
age: i8,
name: String
}
Passing args to commands in cli apps
From std::process::Command
docs:
Note that the argument is not passed through a shell, but given literally to the program. This means that shell syntax like quotes, escaped characters, word splitting, glob patterns, substitution, etc. have no effect.
If you don't pay much attention to how shells work, this will trip you up. A shell like bash
, zsh
, fish
, etc., will interpret what you type in order to pass valid parameters to binaries. For example, when you type ls ~
it turns ~
into /Users/<username>
or where ever $HOME
is, etc.
Hopefully now the Rust quote above makes more sense. If you try to run a command that requires shell expansion, substitution, etc., it will not work. This has gotten me a few times. (1) I've tried to use ~
in a command. (2) I've tried to use >>
in a command.
From and Into
The Rust by Example chapter on From and Into is very short and explains it perfectly, assuming you already know what traits are (if you don't, it has a chapter on traits, too).
I want to emphasize one thing that confused me early on, x.into(y)
is automatically generated from Y::from(x)
. from
and into
are semantically the same function from different perspectives. The only problem here is that into
can turn a type into any other type that has defined a from
for that type. This means you often need to explicitly type the param you are "turning into" so the compiler knows which from
to call. This is easier to demonstrate with code:
struct X {}
struct Y {}
struct Z {}
impl From<X> for Y {
fn from(value: X) -> Self {
Y {}
}
}
impl From<X> for Z {
fn from(value: X) -> Self {
Z {}
}
}
fn demo() {
let x1 = X {};
// will error without type
// because it could also be type `Z`
let y: Y = x1.into();
let x2 = X {};
// same here, could be type `Y`
let z: Z = x2.into();
}
The Prelude Pattern
From the std::prelude
module docs:
The prelude is the list of things that Rust automatically imports into every Rust program. It’s kept as small as possible, and is focused on things, particularly traits, which are used in almost every single Rust program.
This "prelude pattern" is used elsewhere within the standrard library, and libraries have taken to copying it as well.
The difference between ‘the prelude’ and these other preludes is that they are not automatically
use
’d, and must be imported manually. This is still easier than importing all of their constituent components.
For example, std::io::prelude
requires import:
use std::io::prelude::*;
For an example outside std
, the testing library assert_fs uses the prelude pattern to import various testing utilities:
use assert_fs::prelude::*;
use predicates::prelude::*;
Setup with rustup
rustup is both the recommended and only official installation method. Like other areas of the Rust ecosystem, there is a rustup book
./rust-toolchain
is read by rustup
to control the toolchain
rustup help doc
will show you all the docs you can download and view locally.
rustup doc --book
will render the amazing "Rust book" locally in the default browser.
Once you have a local version of rust you can start a new project via cargo init
, etc