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_refof type&T, whereThas 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]) -> &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 code Under 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 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
selfby Reference (Omitting Lifetime Parameters)If a function is an
implmethod on some type and that takes itsselfparameter 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
structConstructorsWhen 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
SelfInside
implblocks, Rust automatically creates a type alias of the type for which theimplblock 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 * yMul::mul(x, y)x += yx.add_assign(y)
The
?operatorUnder the hood, the
?operator callsFrom::fromon 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::Errortrait.
asyncLifetimes
asyncfunction 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
Futurefor the anonymous type and provides apollmethod:#![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
pollis first called, it'll callfut_one's poll. If it's still pending, it'll return. Future calls topollwill pick up where the previous poll left off, based onstate.