Hidden Code

A lot of Rust code is ostensibly terse and clean-looking (especially relative to C++). This is great, but as a Rust newbie, the implicit assumptions made by the compiler can seem a bit blackbox-y, which can make things difficult to reason about.

Here, I'll try to jot down examples of the aforementioned implicit decisions made by the compiler by comparing idiomatic code with it's fully-expressed, verbose syntax.

Return Type Omission

A function declaration whose return type is omited is shorthand for returning the unit type.

The codeUnder the hood
fn my_fn() { .. }fn my_fn() -> () { .. }

Automatic Dereferencing (The . Operator)

The . operator implicitly dereferences its left operand, if needed. e.g. for a reference variable named some_ref of type &T, where T has a field named x:

The codeUnder the hood
some_ref.x(*some_ref).x

Automatic Referencing (The . Operator)

The . operator also implicitly borrows a reference to its left operand, if needed for a method call.


#![allow(unused)]
fn main() {
let mut x = vec![1993, 1963, 1991];
let mut y = vec![1993, 1963, 1991];
x.sort();
(&mut y).sort();
assert_eq!(x, y);
}
The codeUnder the hood
v.sort()(&mut v).sort()

Reference Traversal (The . Operator)

. will follow as many references as it takes to reach its target.


#![allow(unused)]
fn main() {
struct Number { value: usize }
let n = Number { value: 999 };
let r: &Number = &n;
let rr: &&Number = &r;
let rrr: &&&Number = &rr;
assert_eq!(rrr.value, (*(*(*rrr))).value);
}
The codeUnder the hood
rrr.value(*(*(*rrr))).value

Reference Traversal (Comparison Operators)

Rust's comparison operators can also "see through" references, as long as both operands have the same type.


#![allow(unused)]
fn main() {
let x = 10;
let y = 10;
let rx = &x;
let ry = &y;
let rrx = ℞
let rry = &ry;

assert!(rrx <= rry);
assert!(*(*rrx) <= *(*rry));
}
The codeUnder the hood
rrx <= rry*(*rrx) <= *(*rry)

Single Reference Parameter (Omitting Lifetime Parameters)

When a function takes a single reference as an argument, and returns a single reference, Rust assumes that the two must have the same lifetime.

The codeUnder the hood
fn smallest(v: &[i32]) -> &i32fn smallest<'a>(v: &'a [i32]) -> &'a i32

No Return Reference (Omitting Lifetime Parameters)

When a function doesn't return any references, Rust doesn't need explicit lifetimes.

The codeUnder the hood
fn sum_r_xy(r: &i32, s: S) -> i32fn sum_r_xy<'a, 'b, 'c>(r: &'a i32, s: S<'b, 'c>) -> i32

Single Lifetime (Omitting Lifetime Parameters)

If there's only a single lifetime that appears among a function's parameters, Rust assumes any lifetimes in the return value msut be that one.

The codeUnder the hood
fn first_third(point: &[i32; 3]) -> (&i32, &i32)fn first_third<'a>(point: &'a [i32; 3]) -> (&'a i32, &'a i32)

Accepting self by Reference (Omitting Lifetime Parameters)

If a function is an impl method on some type and that takes its self parameter by reference, Rust assumes that self's lifetime is the one to give the method's return value.

The codeUnder the hood
fn find_by_prefix(&self, prefix: &str) -> Option<&String>fn find_by_prefix<'a, 'b>(&'a self, prefix: &'b str) -> Option<&'a String>

Tuple struct Constructors

When defining a tuple-like struct, Rust implicitly defines a function that acts as the type's constructor.

The codeUnder the hood
struct Bounds(usize, usize)fn Bounds(x: usize, y: usize) -> Bounds

Self

Inside impl blocks, Rust automatically creates a type alias of the type for which the impl block is associated called Self.


#![allow(unused)]
fn main() {
impl<T> Queue<T> {
  pub fn new() -> Self { .. }
}
}
The codeUnder the hood
pub fn new() -> Self { .. }pub fn new() -> Queue<T> { .. }

Overloaded Operators

Overloaded operators (even the basic ones) are implicitly calling a method specified by their corresponding generic trait.

The codeUnder the hood
x * yMul::mul(x, y)
x += yx.add_assign(y)

The ? operator

Under the hood, the ? operator calls From::from on the error value to convert it to a boxed trait object, a Box<dyn error::Error>, which is polymorphic -- that means that lots of different kinds of errors can be returned from the same function because all errors act the same since they all implement the error::Error trait.

async Lifetimes

async function lifetimes have different rules if one of its arguments is reference of is 'non-static.

This function:


#![allow(unused)]
fn main() {
async fn foo(x: &u8) -> u8 { *x }
}

Is this under the hood:


#![allow(unused)]
fn main() {
fn foo<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a {
  async move { *x }
}
}

Pinning

A lot happens under the hood when using async. Let's look at this incomplete code, where we run two futures in sequence:


#![allow(unused)]
fn main() {
let fut_one = /* ... */;
let fut_two = /* ... */;
async move {
  fut_one.await;
  fut_two.await;
}
}

Under the hood, rust creates an anonymous type representing the async { } block and its combined possible states:


#![allow(unused)]
fn main() {
struct AnonAsyncFuture {
  fut_one: FutOne,
  fut_two: FutTwo,
  state: State,
}

enum AnonState {
  AwaitingFutOne,
  AwaitingFutTwo,
  Dont,
}
}

Then it implements Future for the anonymous type and provides a poll method:


#![allow(unused)]
fn main() {
impl Future for AnonAsyncFuture {
  type Output = ();

  fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
    loop {
      match self.state {
        State::AwaitingFutOne => match self.fut_one.poll(..) {
            Poll::Ready(()) => self.state = State::AwaitingFutTwo,
            Poll::Pending => return Poll::Pending,
        }
        State::AwaitingFutTwo => match self.fut_two.poll(..) {
            Poll::Ready(()) => self.state = State::Done,
            Poll::Pending => return Poll::Pending,
        }
        State::Done => return Poll::Ready(()),
      }
    }
  }
}
}

When poll is first called, it'll call fut_one's poll. If it's still pending, it'll return. Future calls to poll will pick up where the previous poll left off, based on state.