RFC 1216: bang-type

lang (typesystem | uninhabited)

Summary

Promote ! to be a full-fledged type equivalent to an enum with no variants.

Motivation

To understand the motivation for this it's necessary to understand the concept of empty types. An empty type is a type with no inhabitants, ie. a type for which there is nothing of that type. For example consider the type enum Never {}. This type has no constructors and therefore can never be instantiated. It is empty, in the sense that there are no values of type Never. Note that Never is not equivalent to () or struct Foo {} each of which have exactly one inhabitant. Empty types have some interesting properties that may be unfamiliar to programmers who have not encountered them before.

Here's some example code that uses Never. This is legal rust code that you can run today.

use std::process::exit;

// Our empty type
enum Never {}

// A diverging function with an ordinary return type
fn wrap_exit() -> Never {
    exit(0);
}

// we can use a `Never` value to diverge without using unsafe code or calling
// any diverging intrinsics
fn diverge_from_never(n: Never) -> ! {
    match n {
    }
}

fn main() {
    let x: Never = wrap_exit();
    // `x` is in scope, everything below here is dead code.

    let y: String = match x {
        // no match cases as `Never` has no variants
    };

    // we can still use `y` though
    println!("Our string is: {}", y);

    // we can use `x` to diverge
    diverge_from_never(x)
}

This RFC proposes that we allow ! to be used directly, as a type, rather than using Never (or equivalent) in its place. Under this RFC, the above code could more simply be written.

use std::process::exit;

fn main() {
    let x: ! = exit(0);
    // `x` is in scope, everything below here is dead code.

    let y: String = match x {
        // no match cases as `Never` has no variants
    };

    // we can still use `y` though
    println!("Our string is: {}", y);

    // we can use `x` to diverge
    x
}

So why do this? AFAICS there are 3 main reasons

I suspect the reason that ! isn't already treated as a canonical empty type is just most people's unfamilarity with empty types. To draw a parallel in history: in C void is in essence a type like any other. However it can't be used in all the normal positions where a type can be used. This breaks generic code (eg. T foo(); T val = foo() where T == void) and forces one to use workarounds such as defining struct Void {} and wrapping void-returning functions.

In the early days of programming having a type that contained no data probably seemed pointless. After all, there's no point in having a void typed function argument or a vector of voids. So void was treated as merely a special syntax for denoting a function as returning no value resulting in a language that was more broken and complicated than it needed to be.

Fifty years later, Rust, building on decades of experience, decides to fix C's shortsightedness and bring void into the type system in the form of the empty tuple (). Rust also introduces coproduct types (in the form of enums), allowing programmers to work with uninhabited types (such as Never). However rust also introduces a special syntax for denoting a function as never returning: fn() -> !. Here, ! is in essence a type like any other. However it can't be used in all the normal positions where a type can be used. This breaks generic code (eg. fn() -> T; let val: T = foo() where T == !) and forces one to use workarounds such as defining enum Never {} and wrapping !-returning functions.

To be clear, ! has a meaning in any situation that any other type does. A ! function argument makes a function uncallable, a Vec<!> is a vector that can never contain an element, a ! enum variant makes the variant guaranteed never to occur and so forth. It might seem pointless to use a ! function argument or a Vec<!> (just as it would be pointless to use a () function argument or a Vec<()>), but that's no reason to disallow it. And generic code sometimes requires it.

Rust already has empty types in the form of empty enums. Any code that could be written with this RFC's ! can already be written by swapping out ! with Never (sans implicit casts, see below). So if this RFC could create any issues for the language (such as making it unsound or complicating the compiler) then these issues would already exist for Never.

It's also worth noting that the ! proposed here is not the bottom type that used to exist in Rust in the very early days. Making ! a subtype of all types would greatly complicate things as it would require, for example, Vec<!> be a subtype of Vec<T>. This ! is simply an empty type (albeit one that can be cast to any other type)

Detailed design

Add a type ! to Rust. ! behaves like an empty enum except that it can be implicitly cast to any other type. ie. the following code is acceptable:

let r: Result<i32, !> = Ok(23);
let i = match r {
    Ok(i)   => i,
    Err(e)  => e, // e is cast to i32
}

Implicit casting is necessary for backwards-compatibility so that code like the following will continue to compile:

let i: i32 = match some_bool {
    true  => 23,
    false => panic!("aaah!"), // an expression of type `!`, gets cast to `i32`
}

match break {
    ()  => 23,  // matching with a `()` forces the match argument to be cast to type `()`
}

These casts can be implemented by having the compiler assign a fresh, diverging type variable to any expression of type !.

In the compiler, remove the distinction between diverging and converging functions. Use the type system to do things like reachability analysis.

Allow expressions of type ! to be explicitly cast to any other type (eg. let x: u32 = break as u32;)

Add an implementation for ! of any trait that it can trivially implement. Add methods to Result<T, !> and Result<!, E> for safely extracting the inner value. Name these methods along the lines of unwrap_nopanic, safe_unwrap or something.

Drawbacks

Someone would have to implement this.

Alternatives

Unresolved questions

! has a unique impl of any trait whose only items are non-static methods. It would be nice if there was a way to automate the creation of these impls. Should ! automatically satisfy any such trait? This RFC is not blocked on resolving this question if we are willing to accept backward-incompatibilities in questionably-valid code which tries to call trait methods on diverging expressions and relies on the trait being implemented for (). As such, the issue has been given it's own RFC.