Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pre-RFC: Add an `Empty` type to libcore. Use it to replace the `fn() -> !` syntax. #1001

Closed
canndrew opened this Issue Mar 22, 2015 · 12 comments

Comments

Projects
None yet
7 participants
@canndrew
Copy link
Contributor

canndrew commented Mar 22, 2015

  • Start Date: (fill me in with today's date, YYYY-MM-DD)
  • RFC PR: (leave this empty)
  • Rust Issue: (leave this empty)

Summary

Add an Empty type to libcore. Use it to replace the fn() -> ! sytax.

Motivation

See some previous discussion here: http://internals.rust-lang.org/t/should-be-a-type/1723 here: rust-lang/rust#20172 and here: rust-lang/rust#18414

Edit: @reem has already implemented a crate for this. See: https://github.com/reem/rust-void

A standard Empty type (ie. an enum with no variants) would be a useful addition to the standard library. One use case is if you need to implement a trait method that returns Result<_, Err> where Err is an associated type but you know that your implementation cannot fail. Currently, library authors can easily define their own empty types, but this will result in multiple libraries defining the same thing.

Making diverging functions ordinary functions also allows them to be used in generic code. For example, it would allow functions that diverge to be passed as arguments to higher-order functions.

Removing ! and replacing it with a plain ol' type will make Rust slightly simpler.

Detailed design

Add this to libcore:

[lang="empty"]
enum Empty {}

impl Empty {
    fn elim<T>(self) -> T {
        match self {}
    }
}

Add an empty lang item so that intrinsics like abort can return it. Make all methods that are currently marked as diverging (eg. panic, abort) return Empty instead. Make the panic! macro expand to begin_unwind(...).elim() instead of just begin_unwind(...) to avoid users having to add the .elim() themselves.

Add a convenience method to Result<T, Empty>:

impl<T> Result<T, Empty> {
    fn to_inner(self) -> T {
        match self {
            Ok(x)  => x,
            Err(e) => e.elim(),
        }
    }
}

Drawbacks

This change will require typing .elim() where previously someone wouldn't have had to. But only when calling a diverging function from a non-diverging function without using one of the panic! family macros. In the future, Rust's type checker could be improved to allow any type which is isomorphic to Empty to unify with any other type.

It might make things like reachability checking more difficult. But it would be nice anyway if the compiler could detect unreachable code after code that produces an empty value.

! used to be treated as a type though, but this was removed due to maintenance difficulties.

Alternatives

Just add an Empty type to libcore but keep the seperate divergence syntax aswell.

Name it Never as in "this value can never exist" or "this function can never return". Name it Void. However this is likely to confuse programmers from a C-family language background as Void is not (). Name it Bottom. Name it something else.

Unresolved questions

Is there time to make this change before 1.0?

@llogiq

This comment has been minimized.

Copy link
Contributor

llogiq commented Mar 22, 2015

One other alternative is to keep the ! as syntactic sugar for the not-type.

@glaebhoerl

This comment has been minimized.

Copy link
Contributor

glaebhoerl commented Mar 22, 2015

Make the panic! macro expand to begin_unwind(...).elim() instead of just begin_unwind(...) to avoid users having to add the .elim() themselves.

The advantage of the current built-in support for ! is that it unifies with any other type automatically, without the need for explicit elim() calls, and this works for anything that returns !, not just panic! or other macros. (I.e. this is what would belong in the Drawbacks section.) Until/unless Rust is capable of dealing with a type like for<T> T directly, I would be in favor of keeping this. It's nice.

If we do want to add a library type instead of or in addition to it, I think it should be called Never.

@kennytm

This comment has been minimized.

Copy link
Member

kennytm commented Mar 22, 2015

The bottom type was removed in rust-lang/rust#14973, because of maintenance difficulty?

I don't think it's very important for 1.0 though, requiring the user to provide a must-be-never-ending closure Fn() -> ! seems unusual.

@canndrew

This comment has been minimized.

Copy link
Contributor Author

canndrew commented Mar 23, 2015

The advantage of the current built-in support for ! is that it unifies with any other type automatically, without the need for explicit elim() calls

How often do you call a diverging function from a non-diverging function without using panic! or assert! et. al? Having a whole extra construct in the language which breaks the ability to use things like Fn() -> T traits seems like large price to pay to avoid having to type .elim() occasionally.

and this works for anything that returns !, not just panic! or other macros

And similarly .elim() will work on anything that returns Empty.

@canndrew

This comment has been minimized.

Copy link
Contributor Author

canndrew commented Mar 23, 2015

If we do want to add a library type instead of or in addition to it, I think it should be called Never.

I quite like this. Then the name Empty could be reserved for a trait of types that are empty.

@canndrew

This comment has been minimized.

Copy link
Contributor Author

canndrew commented Mar 23, 2015

I don't think it's very important for 1.0 though

It's only important because it would be a backwards-incompatible change.

@reem

This comment has been minimized.

Copy link

reem commented Mar 23, 2015

From an optimization perspective, the implementation of elim can call intrinsics::unreachable to get LLVM in on the unreachability of calls to elim or any code following a call to elim.

Bikeshed: elim should be called unreachable, since that's what it means.

Note that there exists (disclaimer: I wrote it) a crate with a similar type and helpers called void, whose code is here: https://github.com/reem/rust-void

@reem

This comment has been minimized.

Copy link

reem commented Mar 23, 2015

Also, a similar empty type used to exist in std::any.

@llogiq

This comment has been minimized.

Copy link
Contributor

llogiq commented Mar 23, 2015

@canndrew: Why would it not be backwards-compatible (provided we choose to keep ! as syntactic sugar)? Can you provide an example of a program that would break?

@canndrew

This comment has been minimized.

Copy link
Contributor Author

canndrew commented May 20, 2015

@llogiq I meant if the ! syntax was removed. Too late for that now though.

@llogiq

This comment has been minimized.

Copy link
Contributor

llogiq commented May 20, 2015

Too late for that now though.

Agreed. Well, I think the ! syntax is at least obviously confusing. Whereas any of the suggested names would have been confusing to some, but not all. And reem's implementation seems to handle the cases where the usefulness outweighs the confusion. 😄

@Stebalien

This comment has been minimized.

Copy link
Contributor

Stebalien commented Aug 20, 2016

Triage: Superseded by #1216

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.