Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Rust have a "Never" primitive type?

Tags:

types

rust

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.

like image 233
typesanitizer Avatar asked Aug 14 '18 01:08

typesanitizer


People also ask

Is () a type in Rust?

() is the unit type or singleton type: it has a single value, also denoted () .

Does Rust have reference types?

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.

What is a rust type?

Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters.

What is unit type in Rust?

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.


2 Answers

TL;DR: Because it enables local reasoning, and composability.

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.

Local reasoning for nonsensical code

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.

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:

; 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
}

Composability

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)

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:

#![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.

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.

like image 127
Matthieu M. Avatar answered Oct 12 '22 13:10

Matthieu M.


I think the reasons why Rust needs a special type ! include:

  1. 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.

  2. 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.

like image 23
typesanitizer Avatar answered Oct 12 '22 12:10

typesanitizer