Rust's std::process::exit
has the type
pub fn exit(code: i32) -> !
where !
is the "Never" primitive type.
Why does Rust need a special type for this?
Compare this with Haskell where the type of System.Exit.exitWith
is
exitWith :: forall a. Int -> a
The corresponding Rust signature would be
pub fn exit<T>(code: i32) -> T
There is no need to monomorphize this function for different T
's because a T
is never materialized so compilation should still work.
() is the unit type or singleton type: it has a single value, also denoted () .
Primitive Type reference. References, &T and &mut T . A reference represents a borrow of some owned value. You can get one by using the & or &mut operators on a value, or by using a ref or ref mut pattern.
Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters.
The () type, sometimes called "unit" or "nil". The () type has exactly one value () , and is used when there is no other meaningful value that could be returned.
Your idea of replacing exit() -> !
by exit<T>() -> T
only considers the type system and type inference. You are right that from a type inference point of view, both are equivalent. Yet, there is more to a language than the type system.
The presence of !
allows local reasoning to detect nonsensical code. For example, consider:
use std::process::exit;
fn main() {
exit(3);
println!("Hello, World");
}
The compiler immediately flags the println!
statement:
warning: unreachable statement
--> src/main.rs:5:5
|
5 | println!("Hello, World");
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(unreachable_code)] on by default
= note: this error originates in a macro outside of the current crate
(in Nightly builds, run with -Z external-macro-backtrace for more info)
How? Well, exit
's signature makes it clear it will never return, since no instance of !
can ever be created, therefore anything after it cannot possibly be executed.
Similarly, rustc passes on this information about the signature of exit
to the LLVM optimizer.
First in the declaration of exit
:
; std::process::exit
; Function Attrs: noreturn
declare void @_ZN3std7process4exit17hcc1d690c14e39344E(i32) unnamed_addr #5
And then at the use site, just in case:
; playground::main
; Function Attrs: uwtable
define internal void @_ZN10playground4main17h9905b07d863859afE() unnamed_addr #0 !dbg !106 {
start:
; call std::process::exit
call void @_ZN3std7process4exit17hcc1d690c14e39344E(i32 3), !dbg !108
unreachable, !dbg !108
}
In C++, [[noreturn]]
is an attribute. This is unfortunate, really, because it does not integrate with generic code: for a conditionally noreturn
function you need to go through hoops, and the ways to pick a noreturn
type are as varied as there are libraries using one.
In Rust, !
is a first-class construct, uniform across all libraries, and best of all... even libraries created without !
in mind can just work.
The best example is the Result
type (Haskell's Either
). Its full signature is Result<T, E>
where T
is the expected type and E
the error type. There is nothing special about !
in Result
, yet it can be instantiated with !
:
#![feature(never_type)]
fn doit() -> Result<i32, !> { Ok(3) }
fn main() {
doit().err().unwrap();
println!("Hello, World");
}
And the compiler sees right through it:
warning: unreachable statement
--> src/main.rs:7:5
|
7 | println!("Hello, World");
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(unreachable_code)] on by default
= note: this error originates in a macro outside of the current crate
(in Nightly builds, run with -Z external-macro-backtrace for more info)
The ability to reason about types that cannot be instantiated also extends to reasoning about enum variants that cannot be instantiated.
For example, the following program compiles:
#![feature(never_type, exhaustive_patterns)]
fn doit() -> Result<i32, !> {
Ok(3)
}
fn main() {
match doit() {
Ok(v) => println!("{}", v),
// No Err needed
}
// `Ok` is the only possible variant
let Ok(v) = doit();
println!("{}", v);
}
Normally, Result<T, E>
has two variants: Ok(T)
and Err(E)
, and therefore matching must account for both variants.
Here, however, since !
cannot be instantiated, Err(!)
cannot be, and therefore Result<T, !>
has a single variant: Ok(T)
. The compiler therefore allows only considering the Ok
case.
There is more to a programming language than its type system.
A programming language is about a developer communicating its intent to other developers and the machine. The Never type makes the intent of the developer clear, allowing other parties to clearly understand what the developer meant, rather than having to reconstruct the meaning from incidental clues.
I think the reasons why Rust needs a special type !
include:
The surface language doesn't offer any way to write type Never = for<T>(T)
analogous to type Never = forall a. a
in Haskell.
More generally, in type aliases, one cannot use type variables (a.k.a. generic parameters) on the RHS without introducing them on the LHS, which is precisely what we want to do here. Using an empty struct/enum doesn't make sense because we want a type alias here so that Never
can unify with any type, not a freshly constructed data type.
Since this type cannot be defined by the user, it presents one reason why adding it as a primitive may make sense.
If one is syntactically allowed to assign a non-monomorphizable type to the RHS (such as forall a. a
), the compiler will need to make an arbitrary choice w.r.t. calling conventions (as pointed out by trentcl in the comments), even though the choice doesn't really matter. Haskell and OCaml can sidestep this issue because they use a uniform memory representation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With