Reading Rust

You do not need to write Rust fluently before you can read it. Reading comes first. This page is a decoder ring for the syntax you will see on every page of a Rust codebase.

A function signature, line by line

pub async fn parse_config<'a>(
    input: &'a str,
    strict: bool,
) -> Result<Config<'a>, ConfigError> {
    /* ... */
}
TokenWhat it means
pubPublic. Visible outside this module.
asyncReturns a Future. Must be .await-ed by the caller.
fnThis is a function.
parse_configThe name.
<'a>Generic over a lifetime parameter named 'a.
input: &'a strA borrowed string slice tied to 'a.
strict: boolA boolean by value.
-> Result<Config<'a>, ConfigError>Returns either Ok(Config<'a>) or Err(ConfigError).

Once you can read this, you can read 90% of Rust signatures. The same shape repeats with different parts present or absent.

A struct

#[derive(Debug, Clone, PartialEq)]
pub struct Config<'a> {
    pub host: &'a str,
    pub port: u16,
    pub options: Vec<String>,
}
TokenWhat it means
#[derive(...)]Auto-generated trait impls. Debug enables {:?} printing. Clone enables .clone(). PartialEq enables ==.
pub struct Config<'a>A public struct generic over 'a.
pub host: &'a strField, public, borrowed string tied to 'a.
pub port: u16Field, public, unsigned 16-bit int by value.
pub options: Vec<String>Field, public, owned vector of owned strings.

The visibility (pub) on each field is per-field, not per-struct.

A method (impl block)

impl<'a> Config<'a> {
    pub fn new(host: &'a str) -> Self {
        Self {
            host,
            port: 8080,
            options: Vec::new(),
        }
    }
 
    pub fn with_port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }
}
TokenWhat it means
impl<'a> Config<'a>The methods that follow operate on Config<'a>.
fn new(host: &'a str) -> SelfConstructor. Self is shorthand for the impl's type.
Self { host, port: 8080, ... }Struct literal. host is shorthand for host: host.
fn with_port(mut self, port: u16) -> SelfConsumes self, returns a modified Self. The fluent builder pattern.

Common symbols you will see everywhere

SymbolMeaning
&TShared (read-only) reference to T.
&mut TExclusive (read-write) reference to T.
Box<T>Owned heap allocation containing one T.
Vec<T>Owned growable array of T.
Option<T>Some(T) or None. Optional value.
Result<T, E>Ok(T) or Err(E). Success or failure.
&str vs StringBorrowed string slice vs owned heap string.
&[T] vs Vec<T>Borrowed slice of T vs owned vector.
?Propagate error. Returns early if the Result is Err.
!Macro call (println!, vec!, format!).
_Don't care / placeholder.
'aA lifetime parameter named a.
dyn TraitDynamic dispatch through a trait object.
impl Trait"Some concrete type that implements Trait." Hides the actual type.
where T: TraitConstrains T to implement Trait.
::<...>Turbofish. Explicit type parameter at the call site.

These ~20 symbols cover most Rust. Memorize them.

A real Sail snippet, decoded

From the Sail tour:

pub fn list_catalogs(&self, pattern: Option<&str>) -> CatalogResult<Vec<Arc<str>>> {
    Ok(self
        .state()?
        .catalogs
        .keys()
        .filter(|name| match_pattern(name.as_ref(), pattern))
        .cloned()
        .collect::<Vec<_>>())
}

Decoded line by line:

  • pub fn list_catalogs(&self, ...) — a public method borrowing self.
  • pattern: Option<&str> — optional borrowed string slice. Can be Some("foo*") or None.
  • -> CatalogResult<Vec<Arc<str>>> — returns the crate's local result alias, wrapping an owned vector of refcounted strings.
  • self.state()? — gets the mutex-guarded state. ? propagates any error.
  • .catalogs.keys() — get an iterator over the keys.
  • .filter(...) — keep only the ones matching the pattern.
  • .cloned() — turn &Arc<str> into Arc<str> (cheap, refcount bump).
  • .collect::<Vec<_>>() — gather into a Vec; the _ lets Rust infer the inner type.

Five minutes ago this looked dense. Now it reads as a sentence: "for every catalog name that matches the pattern, clone the refcount, collect into a vec."

The five questions for any Rust file

When you open a file you have not seen:

  1. What does it own? Look for the public structs, enums, and traits.
  2. What does it borrow? Look at function signatures: &T, &str, &[T].
  3. What does it return? Especially: which Result<T, E> types?
  4. What does it depend on? Read the use statements at the top.
  5. Where is the entry point? A main function, a server run, a Handler::handle, an impl SomeTrait.

Five minutes of those questions tells you most of what the file is for.