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 code Under 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 namedsome_ref
of type&T
, whereT
has a field namedx
:
The code Under 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 code Under 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 code Under 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 code Under 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 code Under the hood fn smallest(v: &[i32]) -> &i32
fn 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 code Under the hood fn sum_r_xy(r: &i32, s: S) -> i32
fn 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 code Under 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 itsself
parameter by reference, Rust assumes thatself
's lifetime is the one to give the method's return value.
The code Under 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
ConstructorsWhen defining a tuple-like
struct
, Rust implicitly defines a function that acts as the type's constructor.
The code Under 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 theimpl
block is associated calledSelf
.#![allow(unused)] fn main() { impl<T> Queue<T> { pub fn new() -> Self { .. } } }
The code Under 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 code Under the hood x * y
Mul::mul(x, y)
x += y
x.add_assign(y)
The
?
operatorUnder the hood, the
?
operator callsFrom::from
on the error value to convert it to a boxed trait object, aBox<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 theerror::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 apoll
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 callfut_one
's poll. If it's still pending, it'll return. Future calls topoll
will pick up where the previous poll left off, based onstate
.