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.
TL;DR: Because it enables local reasoning, and composability.
Your idea of replacing
exit() -> !
byexit<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.Local reasoning for nonsensical code
The presence of
!
allows local reasoning to detect nonsensical code. For example, consider:The compiler immediately flags the
println!
statement: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.Local reasoning for optimizations
Similarly, rustc passes on this information about the signature of
exit
to the LLVM optimizer.First in the declaration of
exit
:And then at the use site, just in case:
Composability
In C++,
[[noreturn]]
is an attribute. This is unfortunate, really, because it does not integrate with generic code: for a conditionallynoreturn
function you need to go through hoops, and the ways to pick anoreturn
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'sEither
). Its full signature isResult<T, E>
whereT
is the expected type andE
the error type. There is nothing special about!
inResult
, yet it can be instantiated with!
:And the compiler sees right through it:
Composability (bis)
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:
Normally,
Result<T, E>
has two variants:Ok(T)
andErr(E)
, and therefore matching must account for both variants.Here, however, since
!
cannot be instantiated,Err(!)
cannot be, and thereforeResult<T, !>
has a single variant:Ok(T)
. The compiler therefore allows only considering theOk
case.Conclusion
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 totype 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.