Split Never-type into 2 types

Never type is an not yet stabilized.

My thought - never-type uses in 2 roles now: as "any"! type and as "never"! type.

So, I suggest to split this type into 2 before it is not too late.

Ok, a brief review of splitting Never type into 2 types:

  1. "any"! type,
  • it is never finish to calculate,
  • it is always fallable
  • it is could be converted to any type implicitly.
  • it appears as "result" type only, ... -> !any
  • it never is a part of complex type, like Result<T, !any>, except "result" type
  • in Generics it could be used, but as a "result" type only, ... -> T
  • in Arguments type it could be used, but as a "result" type only, ... -> !any
  • It is mostly used in such expressions loop -> !any, panic!() -> !any, break -> !any
  1. "never"! type,
  • it is "ordinary" type, like enum !never {}
  • but it is never start to calculate.
  • it is never fallable
  • In optimization all code which deals with pure !never type is always "unused" and could be removed
  • it is never be converted/coerced implicitly
  • it is mostly used as complex type, like Result<T, !never>, generics.

Those rules are full and do not contradict each other.

It was discussed here: Zulip > t-lang > what is never type

Names are in discussion.It could be ! as "any"! and "enum Void {}" as "never"!

By the way I looked at rust-parser - we could change from

NeverType : !

into

NeverType : !any | !never | !

EDITED: we could add a rule, that !never could be converted to !any (but not to other types), but I'm not sure about consequences

hist ! means !never unless it's in -> ! in which cases it means what it already means today -> !any;

What advantage would splitting it provide?

2 Likes

In most cases it is easily inferred which role never-type should be used whatever it is used.

Unfortunately, there is an ambiguity in choosing which type in some rare cases and those cases create a lot of errors and feature is not stable.

But if we split it, no ambiguity left, no type-errors and no missuses of never type.

Let me ask an alternate phrasing: what makes you think these are two different roles?

We could look to another language with no magic type. For example, Haskell:

error :: HasCallStack => String -> a

data Void;
absurd :: Void -> a
absurd a = case a of {}

We see, that error function has generic return type a, similar to rust <T> ... -> T type, just like "any"!.

And Void is an uninhabited type, just like "never"!

Swift, too, has only Never and no magic type, only recognition of a caseless enum as uninhabited. But you didn’t suggest getting rid of ! or making it an alias of std::convert::Infallible, both of which have been proposed in the past (with unfortunate complications that make it easier said than done); you suggested having both.

Rust uses both roles, so let's be 2 types: each type for one role.

We could name "never"! as Infallible and "any"! as !. Or other way.

P.S. Swft has "throw" types in signature, but neither Rust nor Haskell has such.

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

What advantage do we get from such a split? What does this let you do that you can't do by writing your own enum VitNever {}?

:innocent: own enum VitNever {} :innocent: :smiling_face_with_three_hearts: :heart_eyes:

It is like to say, why are you not create own MyOption<T>, MyResult<T,E> or MyUnit?

Nothing forbids to use own MyNever, except of optimization: all code which deals with pure !never type is always "unused" and could be removed. It exists for type-checking only.

P.S. "!any" is not "unused", and now the compiler guess of optimization and about the role.

1 Like

!any is unused. That’s why you get unreachable code warnings from code after a panic. The compiler will remove such code today, and it’s correct to do so.

1 Like

As was pointed out in the Zulip thread, you can have ! ("!any") in argument position today (or in the place of a type parameter, etc).

One of the examples, slightly extended. There were other examples that rely on the coercion properties of the aliased !.

So a lot of these would be a breaking change (or there are multiple !any types I guess, but I don't see the point).

I agree

First, !any is a type and it cannot be "used" or "unused"

Second, "used"/called or "unused"/uncalled is a code, a function. Function "panic" is definitely is called, so it is "used"

Interesting, this example was added, before I add a remark :

  • in Generics it could be used, but as a "result" type only, ... -> T
  • in Arguments type it could be used, but as a "result" type only, ... -> !any

Now this example it totally Ok for T=!any (except function any_in_args())

pub trait Ret {
    type Ty;
}

impl<T> Ret for fn() -> T {
    type Ty = T;
}

pub type Any = <fn() -> !any as Ret>::Ty;

pub fn any_in_args2(_: !never) -> Any {
    panic!()
}

// this is must not compile - Argument cannot have type !any
pub fn any_in_args(_: Any) -> Any {
    panic!()
}

You are probably thinking of Haskell lazy semantics. Rust is a strict language and there is no concept of a value that has started but hasn't finished calculating. A function is what calculates, not a value of some type. Once you create a value of a type, it has already "finished" being created.

So this distinction is meaningless.

! is also an uninhabited type. If what you mean is that it's impossible to write a Haskell expression that has type Void then it's just false -- here is one:

void :: Void
void = void
3 Likes

You mentioned Rust having <T> ... -> T, why is that not enough and you need this !any?

Overall the proposal seems very lacking of a reasons we may want this split. The linked zulip thread doesn't go into the reasons either.

3 Likes

You think that "bottom"! type is "infinite calculation". Ok. Empty loop is an "infinite calculation".

But panic!() is definitely not an "infinite calculation", and it is not an "bottom"! type, it is "any"! type.

Why do you use "bottom"! instead of "any"! ?

For Rust, for practical purpose I see it is useless to have a "bottom"! type.

I think you misunderstood me. Let's see at function:

fn foo<T>(r: Result<T, !never>) -> T {
    match r {
      Ok (x) => x,
      Err(y) => match y {} // this line is never start to calculate
    }
}

And for practical purpose, line Err(y) => match y {} is never even start to calculate!

In type theory, the bottom type is a subtype of every other type. In other words, bottom is what you call "any".

In Rust it's a bit different because ! isn't technically a subtype of other types, rather it implicitly coerces to other types, but it's the same idea.

5 Likes

Sure, we could have <T> ... -> T instead of !any, but this breaks backward compatibility, because we must change all expression, which already use !any into:

loop<SomeType> { /*...*/ };
return<SomeType> value;
panic!<SomeType>();

Because even now now rust has self-contradiction in behavior of never type and never type is still unstable.

I suggest to add simple rules, that do not contradict each other

In rust we do not care if "bottom"! is a part of "any"! or not (spoiler: it is, but we do not care)

And NO, what I call "any" is not a real bottom type, because panic!() is not an "infinite calculations", but halting a program. This only true for empty loop {}.

1 Like

The bottom type does not necessarily mean "infinite calculation". A type doesn't describe a calculation at all, types represent sets of values. The bottom type is the empty set of values.

It makes sense for a function that never returns to return bottom.

9 Likes