A demonstration of ways to use the "never" type (`!`) in main

Disclaimer

This post is meant as a humorous exploration of the experimental never type !. For most Rust developers, returning () or Result from main is satisfactory - which is great - but is purposely ignored for this post in the spirit of never, which is fine because its never returned anyway.

I do not endorse nor discourage the ! type's use. I find it to be just another negotiation between you and the compiler, for good or for worse, at your discretion.

Enjoy as we never follow the white rabbit!

The Crate

I have been experimenting using the never ! type as the main return type to more easily deal with Results. The reason I decided on using the never type - rather than having main return a Result - is because I want control with how my process exits.

I would like to show how code can be written by only changing how the never type gets returned from main.

I'll start off with an example crate with main using a typical std::io::Result<()>:

main.rs

#![feature(never_type)]

use std::{
    fs::File,
    io::Read,
    process::exit
};

/// Custom Exit Procedure
fn exit_with_error(code: i32, cause: String) -> ! {
    eprintln!("hash_compress: `{}`", cause);
    exit(code);
}

/// Error Codes
const UNEXPECTED_DIR: i32 = 2;

fn main() -> std::io::Result<()> {
    let file = File::open("input.txt")?;

    let meta = file.metadata()?;

    if meta.is_dir() {
        exit_with_error(UNEXPECTED_DIR, "Expected file, found directory instead".into())
    }

    process_file(file)?;

    Ok(())
}

We will use this as our foundation. For the rest of the examples, we'll keep track of our error codes and the main function.

Note: It is by intention that this example returns a result alongside the design decision of "panic when failed".

Returning !

Let's change the signature of fn main() -> Result<()> {...} to fn main() -> ! {...}.

This has two main consequences:

  1. This means that main "never" returns, since it returns never. Implicitly, it means that main must not return. So, we have to use a function like exit(..) -> ! at the end of main to compile.
  2. We must handle each result. Since we are already exiting upon a failure, we can just use unwrap_or_else with our custom exit procedure upon failure.

Here is that implementation:
updated main.rs

...
/// Error Codes
const SUCCESS: i32 = 0;
const FILE_OPEN_ERROR:i32 = 1;
const UNACCESSABLE_META:i32 = 2;
const UNEXPECTED_DIR:i32 = 3;
const BAD_PROCESS: i32 = 4;

fn main() -> ! {
    let file = File::open("input.txt")
        .unwrap_or_else(|e| exit_with_error(FILE_OPEN_ERROR, e.to_string()));

    let meta = file.metadata()
        .unwrap_or_else(|e| exit_with_error(UNACCESSABLE_META, e.to_string()));

    if meta.is_dir() {
        exit_with_error(UNEXPECTED_DIR, "Expected file, found directory instead".into())
    }

    process_file(file)
        .unwrap_or_else(|e| exit_with_error(BAD_PROCESS, e.to_string()));

    exit(SUCCESS)
}

Returning Result<(), !>

Let now refactor the fn main() -> ! {..} to fn main() -> Result<(), !> {..}. The only difference is seems to be semantics. This more explicitly state that if main succeeds, then its success is infallible, rather than "never" returning.

We can now simply exit with Ok(()):
updated main.rs

...
/// Error Codes
const SUCCESS: i32 = 0;
const FILE_OPEN_ERROR:i32 = 1;
const UNACCESSABLE_META:i32 = 2;
const UNEXPECTED_DIR:i32 = 3;
const BAD_PROCESS: i32 = 4;

fn main() -> ! {
    let file = File::open("input.txt")
        .unwrap_or_else(|e| exit_with_error(FILE_OPEN_ERROR, e.to_string()));

    let meta = file.metadata()
        .unwrap_or_else(|e| exit_with_error(UNACCESSABLE_META, e.to_string()));

    if meta.is_dir() {
        exit_with_error(UNEXPECTED_DIR, "Expected file, found directory instead".into())
    }

    process_file(file)
        .unwrap_or_else(|e| exit_with_error(BAD_PROCESS, e.to_string()));

    exit(SUCCESS)
}

If we want to really abuse this system, we can replace each unwrap_or_else with a map_err and use the ? symbol to unwrap the Err variant, which is safe since Err will never happens:

...
/// Error Codes
const SUCCESS: i32 = 0;
const FILE_OPEN_ERROR:i32 = 1;
const UNACCESSABLE_META:i32 = 2;
const UNEXPECTED_DIR:i32 = 3;
const BAD_PROCESS: i32 = 4;

fn main() -> Result<(), !> {
    let file = File::open("input.txt")
        .map_err(|e| exit_with_error(FILE_OPEN_ERROR, e.to_string()))?;

    let meta = file.metadata()
        .map_err(|e| exit_with_error(UNACCESSABLE_META, e.to_string()))?;

    if meta.is_dir() {
        exit_with_error(UNEXPECTED_DIR, "Expected file, found directory instead".into())
    }

    process_file(file).map_err(|e| exit_with_error(BAD_PROCESS, e.to_string()))?;

    Ok(())
}

Rust's Rich Type System

There's a problem! Returning Result<(), !> might not be enough! We can see this issue by going across each case exhaustively.

When Err is returned, there are only 2 possible reasons:

  1. We were unsuccessful, but never returned successfully (exit code == 0)
  2. We were unsuccessful, but never returned unsuccessfully (exit code != 0)

When Ok is returned, there is only 1 reason

  1. it was infallible.

This only encapsulates 3 reasons for failure, but don't fear! The beauty of Rust is your ability to make the type system as rich as you please! This is no exception.

Returning Result<!, !>

This is indeed a richer type than Result<(), !>, as now we can encapsulate 4 different reasons for failure and communicate intentions to the reader!
These four possibilities are the following:

  1. We were unsuccessful, but never returned successfully
  2. We were unsuccessful, but never returned unsuccessfully
  3. We were successful, but never returned successfully
  4. We were successful, but never returned unsuccessfully

Now, we can communicate to readers of our code when exiting is successful versus unsuccessful by explicitly using Ok or Err, which will each never happen.

updated main.rs

/// Error Codes
const SUCCESS: i32 = 0;
const FILE_OPEN_ERROR:i32 = 1;
const UNACCESSABLE_META:i32 = 2;
const UNEXPECTED_DIR:i32 = 3;
const BAD_PROCESS: i32 = 4;

// Ignore this attribute.
// Removing this will only stop you from greatness!!!
#[allow(unreachable_code)]
fn main() -> Result<!, !> {
    let file = match File::open("input.txt") {
        Ok(file) => file,
        Err(e) => return Ok(exit_with_error(FILE_OPEN_ERROR, e.to_string())),
    };

    let meta = match file.metadata() {
        Ok(meta) => meta,
        Err(e) => return Ok(exit_with_error(UNACCESSABLE_META, e.to_string())),
    };

    if meta.is_dir() {
        return Ok(exit_with_error(UNEXPECTED_DIR, "Expected file, found directory instead".into()))
    }

    process_file(file).map_err(|e| exit_with_error(BAD_PROCESS, e.to_string()))?;

    Ok(exit(SUCCESS))
}

Conclusion

In the end, irony is a circle.

All jokes aside, I do find the never type to be genuinely useful for personal projects to remind myself to not always care about every little error.

1 Like

Screw all those "SAFETY:" comments. This is what I'll be decorating my unsafe { }'s with from now on.

3 Likes