There's the code:
fn main() {
return; // return -> () ???
}
- What does
return
return without explicitly specifying a value? - Is it true that expressions return a unit type or never type (break, continue) by default?
There's the code:
fn main() {
return; // return -> () ???
}
return
return without explicitly specifying a value?If "return;" is a statement, how does it return a value? Isn't the return done by the implicit last expression which is an empty tuple?
fn main() -> () {
return; // don't return value
() // implicit return value
}
or
fn main() -> () {
return; // same as `return ();`
}
Sorry, return
, break
and continue
evaluate to the !
(never) type:
break
,continue
andreturn
expressions also have type!
.
!
can be coerced to any other type, including the unit type, which is why my previous answer worked, it simply coerced !
to ()
.
return
is an expression, return;
is a statement.
fixed the question
return
is the same as return ()
, and function return type is the same as -> ()
written explicitly.
()
is a zero-sized "nothing" type and ()
is also a value of this type.
Okey, but what's really going on here?
fn main() {
return;
}
return
causes the function to exit early, leaving !
as the return value. The !
value is then coerced to a empty tuple. Am I right?
There is a difference between the return value and the value that return
evaluates to. The return
expression evaluates to !
, whereas the return;
statement returns ()
from the function. I.e. this compiles:
fn main() -> () {
let _: ! = return;
}
but this fails
fn main() -> ! {
let _: ! = return;
}
with the following error:
error[E0069]: `return;` in a function whose return type is not `()`
--> src/main.rs:4:16
|
4 | let _: ! = return;
| ^^^^^^ return type is not `()`
For more information about this error, try `rustc --explain E0069`.
The return expression causes execution of the function to end early. It never gets to the implicit last expression.
!
is a magic type that can be substituted for (almost) any other type.
let x: String = panic!();
!
is the never type. You can never create/instantiate a value with this type and that is it's purpose.
It's how the type system reasons about code like:
fn example(value: u32) {
let percentage: u32 = if value > 100 {
panic!("value too big")
} else {
value
};
println!("{percentage}")
}
panic!()
is the never type (same as return
) which is how the compiler allows that block to evaluate to a u32 type, even though there's a branch that doesn't return a u32. If that branch is followed, it'll never return anything because it instead panics.
This is a distinct concept from ()
, the unit type. The unit type is possible to return, but there's no data there at all.
The following won't compile:
fn bad_example(value: u32) {
let percentage: u32 = if value > 100 {
println!("value too big")
} else {
value
};
println!("{percentage}");
}
The ()
returned from println!()
is assigned to percentage, whereas the !
never type is never returned and never gets assigned.
Examples in the playground
Admittedly these examples are a bit contrived
EDIT: when I say panic!() "is" the never type, I am deliberately avoiding saying panic!() "returns" the never type (how could it? you can never make a never type to return) but "is" might be confusing too. Don't read too much into my choice of the word "is" there.
fn main() {
return;
}
is indeed sugar for
fn main() -> () {
return ();
}
and it works the same way as something like
fn not_main() -> bool {
return false;
}
works.
The return
keyword is used for early returns in Rust. Early returning is one of multiple primitives in Rust that can break ordinary control-flow. Other such examples are panicking; and things like break
/continue
.
return ()
is an expression of type !
. The type of an expression is the type of the value the expression will evaluate to. So "return ()
is an expression that evaluates to a value of type !
" Sometimes colloquially, this is also called the value the expression "returns", however as you may notice, that's confusing, because there's also the early-return from a function via return …
expressions, so let's avoid that confusion and speak of "evaluating to" instead.
The type !
has no values. So the whole statement of "return ()
is an expression that evaluates to a value of type !
" is a sort of fiction. It can never actually evaluate to a value of type !
, because there are no values of type !
.
Why this complicated fiction? It's actually simpler this way. By just assuming that "every expression can evaluate to a value", we keep things uniform. Expressions that don't evaluate to a value are just ordinary expressions, still, and it's only the type that ultimately gives the information that there won't be any value, after all.
About your question of 'coercion', !
can coerce to ()
indeed, something like this is accepted by the compiler:
fn not_main() -> bool {
let coerced_val: () = return false;
}
but a value of type !
will never be coerced to a value of type ()
, because !
has no value.
In the example of
fn not_main() -> bool {
return false;
}
or
fn main() -> () {
return ();
}
arguably no coercion ever needs to happen. A statement like return false;
(mind the semicolon which makes this into a statement) just evaluates the expression return false
and ignores its result whatever type it is, the type of the expression needs not match the return type of the function. Now, if you instead wrote
fn not_main() -> bool {
return false
}
without a semicolon, then the type checker would indeed check "can !
coerce to bool
?" because now return false
is itself in the position where the whole body of the function evaluates to this expression (answer: yes, the type !
can coerce to bool
[or any type, really]; an actual coercion will never happen at run-time because !
-values do not exist)
Speaking of types without a value: There is also a sense in which ()
has no "value", the "unit" type is quite "empty" itself, but that's a different kind of "empty". Indeed ()
has a value, it's just a single value, the value ()
(perhaps confusingly using the same syntax as its type). The information here however is none. A single value is only a single choice, you need zero bits to encode this. So in a very different sense of "does not exist", a value of "()" also "does not exist" at run-time. Yes, using natural language (English) to talk about technically precise stuff can be confusing.
If you happen to undestand Rust enum
s, then !
is like
enum Never {}
and ()
is like
enum Unit {
Unit
}
and the ()
expression would then be Unit::Unit
.
So, given an expression of type !
can never evaluate to any value. What can happen during evaluation, instead then!? There are two possibilities:
loop {}
expression without any break
will have type !
.bar(x)
in foo(bar(x))
is passed on to foo
, in let var = bar(x);
it's assigned to var
, etc. Breaking control flow means that all this normal context is skipped and never executed. The program instead goes somewhere else; which can be, for example
return …
expressionsloop { }
or while … { }
or for … in … { }
) is evaluated, when break
or break …
is usedcatch_unwind
was used, when panickingAlso note: Sort-of "on the way" to that location for the program to continue at, often also some destructors are executed first.
An expression of any so-called "inhabited" type correspondingly has 3 possibilities: (1) evaluation completes and produces a value, (2) evaluation never ends, (3) evaluation ends breaking ordinary control flow.
If you count each different value of the type as their own possibilities, then it's more. A bool
expression can evaluate to true
or to false
or never terminate or panic or break control flow. Yes, some people probably like to differentiate the last two cases, too. The more possible values, the more possibilities; with bool
having 2 values, ()
having 1 value, !
having 0 values, it's unsurprising that evaluation to type !
has fewer possible outcomes.
Also note that across function boundaries, panicking is the only way of "breaking control flow". You can't write break
in a function and expect a loop outside the function to be exited. So if you speak about "what can happen executing a function of return type !
", only the possibilities of "never stops executing" and "panics" are left.
Another complication: If you look back at something like
fn not_main() -> bool {
return false;
}
the return false
is an early-return of type bool
, but where is the non-early return? If the compiler treats !
like any other type, why doesn't it complain unless we gave it also a return value for the normal control flow, e.g. like the following?
fn not_main() -> bool {
return false;
true
}
Answer: There is special handling of !
in the compiler after all. Normally, a function body (or any other block) without a final result-expression that it can evaluate to, can only evaluate to ()
implicitly.
E.g.
let x = {
let y = foo;
bar(y);
};
is like sugar for
let x = {
let y = foo();
bar(y);
()
};
and x
gets assigned ()
.
But if the compiler can determine that a function body (or any other block) will have to evaluate an expression of type !
, and there's no way around that, then the whole block can have type !
(and thus coerce to any type) instead. So e.g. this works, too
let x: String = {
let y = foo();
bar(y);
if foo() {
return;
} else {
panic!();
}
foo();
};
even though there's never any String
produced anywhere; the type-checker can still accept it.
It certainly is not desugaring to
let x: String = {
let y = foo();
bar(y);
if foo() {
return;
} else {
panic!();
}
foo();
() // <- new line added
};
as that wouldn't compile.
Instead it's a bit like if it desugared to something like this instead:
let x: String = {
let y = foo();
bar(y);
if foo() {
return;
} else {
panic!();
}
foo();
unreachable!()
};
where unreachable!()
is giving an explicit !
-typed result expression for the block. But the compiler only does this if this place was really for sure known to already be unreachable anyways; whereas the unreachable!()
macro works simply by panicking. [Though common practice considers it always a bug in your program, if that panicking can ever happen at run-time in your program.]
Same applies to
fn not_main() -> bool {
return false;
}
which you can - if you find this useful - soft-of think of to be 'desugaring' to something like
fn not_main() -> bool {
return false;
unreachable!()
}
which also means there kind-of is something akin to a "coercion from !
to bool
" here, after all.
Coming back to the original
fn main() {
return;
}
the same applies, here, too, but for ()
instead of bool
.
I'll note that the Rust reference is wrong about this:
The type of a block is the type of the final operand, or
()
if the final operand is omitted.
It's missing the case where the end of the block is unreachable.
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.