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

wishlist: Way to express Fn trait like (FnOnce() -> !) #1120

Closed
pnkfelix opened this issue May 13, 2015 · 24 comments
Closed

wishlist: Way to express Fn trait like (FnOnce() -> !) #1120

pnkfelix opened this issue May 13, 2015 · 24 comments

Comments

@pnkfelix
Copy link
Member

Spawned off of rust-lang/rust#25325

As written by @diwic originally:

Here's a minimal example that shows the problem:

    fn never_return<F: FnOnce() -> !>(f: F) -> ! {
        f();
    }

which fails with:

    error: expected type, found `!`
         fn never_return<F: FnOnce() -> !>(f: F) -> ! {

...AFAICS, there's no good reason this syntax is not allowed.

@reem
Copy link

reem commented May 14, 2015

It is possible to fake this using a "void" enum like enum Void { } as the return type and providing an fn unreachable<T>(_: Void) -> T { unsafe { ::std::intrinsics::unreachable() } }, cf. github.com/reem/rust-void for an example.

@sfackler
Copy link
Member

You don't need any unsafe code there, fn unreachable<T>(v: Void) -> T { match v {} } will suffice.

@reem
Copy link

reem commented May 14, 2015

@sfackler interesting, does that have the same optimization results (i.e. does LLVM know that this unreachable is actually unreachable)?

@sfackler
Copy link
Member

Yep: http://is.gd/JVCrHf

enum Void {}

impl Void {
    fn unreachable(&self) -> ! {
        match *self {}
    }
}

O0 IR:

%Void = type {}

; Function Attrs: noreturn uwtable
define internal void @_ZN4Void11unreachable20h428ace37d3624b8ehaaE(%Void* noalias readonly) unnamed_addr #0 {
entry-block:
  %self = alloca %Void*
  store %Void* %0, %Void** %self
  %1 = load %Void** %self, !nonnull !0
  unreachable

join:                                             ; No predecessors!
  unreachable
}

@diwic
Copy link

diwic commented May 14, 2015

Cool, thanks for bringing it up further.

I'm not familiar with the empty enum type, and I'm quite surprised it's allowed by the Rust language at all. As I understand it, the trick is that it's impossible to create a Void so therefore the closure must panic or loop infinitely - but it's entirely possible to create a Void and have the closure just return that (although it's unsafe):

struct A; let b: Void = unsafe { std::mem::transmute(A) }; b

Also I'm not sure why the unreachable function is needed at all - this works fine too:

enum Void {}

fn never_return<F: FnOnce() -> Void> (f: F) -> ! {
    f();
    unreachable!();
}

@glaebhoerl
Copy link
Contributor

although it's unsafe

For a very good reason.

@pnkfelix
Copy link
Member Author

@diwic writes:

but it's entirely possible to create a Void and have the closure just return that (although it's unsafe):

The fact that such a transmute works today is an implementation artifact that, in an ideal world, would be statically rejected; see also #1076 and rust-lang/rust#4499

In particular, the memory layout for enum without a repr attribute is entirely up to the compiler, and such transmutes risk undefined behavior (UB).

@canndrew
Copy link
Contributor

Relevant: #1001

Is there a reason not to make ! a plain ol' type?

@pnkfelix
Copy link
Member Author

@canndrew I woudn't want ! to be a "plain ol' type".

For example, it does not make sense to use it in many contexts where you should be able to put "normal types", such as argument positions for functions.

As I said on another ticket, if we were to add ! as a type, I would want use of it to require a ?-marker, something like ?Converge. But I am not yet convinced there is sufficient value in re-adding it again.

@glaebhoerl
Copy link
Contributor

it does not make sense to use it in many contexts where you should be able to put "normal types"

Doesn't make sense in what way? Merely not useful to do so, or nonsensical (cannot work) at some deeper level?

if we were to add ! as a type, I would want use of it to require a ?-marker, something like ?Converge.

For what reason? (Sorry, you may have already answered this somewhere - but not on the ticket you linked.)

At a technical level, let me put forth the following proposal as a straw man: (1) ! should be nothing more than special syntax for a particular empty enum type (perhaps defined as a lang item); and (2) there should be an automatic coercion from this type to every type. This at least resolves the "specialness" problem, because (as far as I know) empty enum types can appear anyplace where any other type could appear. The drawback of using one would be that it needs to be eliminated explicitly (with e.g. an unreachable()), which is resolved by adding the coercion. In what way would this not be satisfactory?

@canndrew
Copy link
Contributor

Sure it makes sense. What's wrong with writing

fn id(x: !) -> ! {
    x
}

True, you could never call this function so there's no point in writing it. But it has a meaning so there's no point in disallowing it either. There are times where it would be useful for ! to be a type. Maybe I want to return a Result<T, !> from a function that has to return a Result but which I know can never fail. Or maybe I want Vec<!> for a Vec that's that guaranteed to have no elements. Or maybe I want to write FnOnce() -> ! to refer to the trait of functions that never return.

As I said on another ticket, if we were to add ! as a type, I would want use of it to require a ?-marker, something like ?Converge.

What's the reason for this?

But I am not yet convinced there is sufficient value in re-adding it again.

What's the value in not having it? It just makes the language more complicated. I mean, imagine if we took some other type (eg. ()), gave it some special syntax (let's say #), but arbitrarily decided that it's not "really" a type so you can only use it an few places that you can use any other type, (like here: fn() -> #, but not here: Result<i32, #>). And then we go to the extra effort of specially coding this usage into the compiler. Wouldn't that be insane? That may sound contrived but it's exactly what C does with void.

@canndrew
Copy link
Contributor

Sorry, I hadn't seen @glaebhoerl's very similar comment when I posted mine.

@canndrew
Copy link
Contributor

That may sound contrived but it's exactly what C does with void.

In case it's not obvious what I'm saying here:

Rust fixes C's mistake of not treating empty structs like plain ol' types and instead giving them their own weird syntax and semantics that breaks generic code.

But then rust goes and makes the exact same mistake with empty enums. If we can fix this, we should.

@pnkfelix
Copy link
Member Author

@glaebhoerl wrote:

it does not make sense to use it in many contexts where you should be able to put "normal types"

Doesn't make sense in what way? Merely not useful to do so, or nonsensical (cannot work) at some deeper level?

I will concede that, in theory, many mental models of what "types" denote can be made to work with the !-type. (e.g. in "types-as-propositions", ! classifies a particular set of expressions that definitely diverge; in "types-as-sets-of-values", ! denotes the empty set. Both of these models seem to accommodate the addition fine.)

My problem is that on the implementation side of Rust, I worry about e.g. code-gen for a function that takes an argument of type !.

  • Would we actually generate code for such a function? (Would we continue our existing pattern of representing them, like zero-variant enums, by a zero-sized type? Note that I regard that as an anti-pattern...)
  • Am I going to end up adding special cases all over the compiler for for this odd new type variant?

From skimming over the PR where this was removed: PR #17603. I would like to infer that the impact on the compiler would be minimal (and thus I would withdraw the above misgivings).

  • The problem is that I don't think it is necessarily valid to draw that conclusion -- allowing ! in arbitrary type positions would represent a generalization that may expose problems that were not dealt with at the time prior to PR #17603

@pnkfelix
Copy link
Member Author

@glaebhoerl wrote:

if we were to add ! as a type, I would want use of it to require a ?-marker, something like ?Converge.

For what reason?

Because I believe the real-world usage of ! would be limited, and therefore a bound like this would help catch bugs.

Just my opinion; i don't have any data to support it.

@Diggsey
Copy link
Contributor

Diggsey commented May 20, 2015

I think that would be misleading because it implies that a Converge bound guarantees termination, which isn't the case.

@pnkfelix
Copy link
Member Author

@Diggsey yes okay, the choice of name was probably not ideal. (That's sort of a separate topic as to whether using such default bound is a good idea at all; I am willing to concede that both the name and the idea are potentially bad.)

@canndrew
Copy link
Contributor

Would we actually generate code for such a function?

Ideally, any code that handles a value of type ! is dead code and should be eliminated. If a function needs code generated (because it's being exported from a library or something) then just implement it as a call to unreachable!().

Would we continue our existing pattern of representing them, like zero-variant enums, by a zero-sized type? Note that I regard that as an anti-pattern...

From the user's point of view, values of type ! shouldn't have a representation. Any attempt to inspect their representation (for example, using size_of) should be a static error. From the compiler's point of view, if it ever needs to know the size of ! then it's already gone down the rabbit hole into the world of undefined behavior so the "size" you use is pretty much arbitrary. For example, if it needs to push size_of::<!> bytes onto the stack then who cares whether it pushes 0 bytes, 4 bytes or whatever; the user must have done something stupid with unsafe so this code can do whatever it wants.

However this all seems moot because in 1.0 empty types have size 0 and I don't see how that can be changed without breaking backwards compatibility.

Am I going to end up adding special cases all over the compiler for for this odd new type variant?

No, because it's not new. We already have empty types. If you moved @reem's rust-void into libcore and made ! an alias for Void then you could remove all the special casing for the current ! type. You'd also need to make empty types unify with all other types but that's something that would be useful to add to the language anyway.

@pnkfelix
Copy link
Member Author

However this all seems moot because in 1.0 empty types have size 0 and I don't see how that can be changed without breaking backwards compatibility.

This is not quite true; you cannot tag empty enums with #[repr(C)], and thus their representation is considered undefined for the purposes of stability and backwards-compatibility.

@canndrew
Copy link
Contributor

canndrew commented Jun 2, 2015

This code compiles and runs though:

use std::mem;
enum Void {}
fn main() {
    assert_eq!(0, mem::size_of::<Void>());
}

Or are you saying you'd be okay with breaking this behavior?

@glaebhoerl
Copy link
Contributor

If we guaranteed the stability of size_of return values, we'd effectively be forbearing from ever changing our current data representations except in minor ways, whereas we explicitly leave the representation of everything not #[repr(C)] undefined because we wish to reserve the right to change them. (By which I'm just predicting that @pnkfelix's answer will be "yes" - but I'll leave it to him to make that come true or false.)

@canndrew
Copy link
Contributor

canndrew commented Jun 2, 2015

Ah yeah, of course.

@canndrew
Copy link
Contributor

canndrew commented Jun 2, 2015

I just noticed this old RFC of @glaebhoerl's for anonymous enums. If we implemented that then ! could just be the syntax for the empty anonymous enum type.

@Stebalien
Copy link
Contributor

Triage: Fixed by #1216

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants