4 ms·
The lesson here is that implicit conversions are bad. The never type itself having implicit conversions to other types is bad enough (even though such coercions
by kccqzy 4d ago
The lesson here is that implicit conversions are bad. The never type itself having implicit conversions to other types is bad enough (even though such coercions are logically valid: “ex falso quodlibet” they should be explicit), but having a fallback type when type inference doesn’t have enough information to produce a type is even worse. Rust is famous for not even having implicit numeric coercions (say from i8 to i32) but it seems like a shortsighted decision to allow implicit coercions here.
- jadenPete 4d agoWhy is it bad? Implicit integer conversions are generally bad because they can produce unexpected behavior at runtime and obstruct what’s really happening, but that doesn’t seem to be what’s happening here. Never is a standard type in many languages and is at the bottom of the type hierarchy because it’s a subtype of every type. Never isn’t implicitly converted any more than `&’a A` is “implicitly converted” into a `&’b B`, where `’a` subsumes `’b`. There’s no runtime conversion because there will never be an instance of never—it represents the value of a computation that never completes by definition. I think what you mean to say is that implicit runtime conversions are bad, not that all subtyping is bad.
- kccqzy 4d agoNo I’m not talking about runtime conversions. I’m talking about conversions that happen at type inference time. Rust is not a subtyping based language, except for traits and lifetimes. So statements like never being at the bottom of the type hierarchy is irrelevant here even though it is correct. If Rust had higher rank types the never type is also (forall a. a) but still it doesn’t matter. It is simply surprising for a type to be converted implicitly according to subtyping rules other than for traits and lifetimes.
- SabrinaJewson 4d agoDo you have an example of a piece of code that behaves in a surprising way because of this rule?
- kccqzy 4d agoI don’t need to write examples because the article has plenty. All the fixes that Waffle needs to fix are precisely the code that behaves in a surprising way.
- kibwen 4d agoNone of this has anything bad to say about coercion or fallback, it's a consequence of the fact that Rust is an expression-oriented language which had expressions (like `loop {}`) which logically evaluated to the never type when in return position, and yet did not have the machinery in place to support it as a proper concept anywhere outside of return position, and so they chose the unit type as a relatively benign alternative in those contexts, which caused no problems whatsoever until the day came when they decided to actually implement the never type.
- kccqzy 3d ago> so they chose the unit type as a relatively benign alternative in those contexts This is what the article defines as fallback. So the issue has everything to do with fallback.
- kibwen 4d agoLet's avoid using the term "subtyping", which as you say is irrelevant here. The reason you need diverging functions to satisfy arbitrary type obligations (i.e. to coerce to any other type) is because otherwise anything as simple as `let x = Some(42); x.unwrap();` just completely fails to compile, because `unwrap` is internally just: fn unwrap<T>(t: Option<T>) -> T { match t { Some(foo) => foo, None => panic!() } } ...and this function couldn't otherwise typecheck because it doesn't return a `T` in the `None` branch. You need coercion here.
- kccqzy 4d agoNo you don’t need coercion. You only need polymorphism. The type of `panic!()` could be an arbitrary U, which unifies just fine with the type T here. Generally languages with such polymorphism have a never type only because they don’t also support impredicative polymorphism.
- kibwen 4d agoAnd then once you have `fn foo<T>() -> T`, what do you write in the body that allows it to typecheck?
- kccqzy 3d agoYou still write `panic!()`. It type checks using polymorphism only, without any coercions.
- kibwen 3d agoBut `panic!()` is just a macro invocation that needs to expand to something, and the question here is what that something ought to be in order to produce a valid program. Currently it expands to this: https://github.com/rust-lang/rust/blob/98fd715edd3a0a5aa8f2041d7e20a3143b0fc0d3/library/core/src/panicking.rs#L138 https://github.com/rust-lang/rust/blob/98fd715edd3a0a5aa8f20... , which is a function with a return type of `!`, which has indicated a diverging function since long before Rust even considered having a first-class never type.
- SabrinaJewson 4d agoYou’re using “subtype” in two distinct, but related, senses here, and I think this should be clarified. From a more category-theoretic perspective, a type A is a “subtype” of a type B when there is an embedding of A inside B. In this sense, `!` is a subtype of every type (which is its universal property). But this definition also grants you that `String` is a subtype of `BigInt`, because strings can be coded as bit sequences which can be coded in `BigInt`, which may or may not be what you expect. From a programming languages perspective – and this is the terminology generally used in Rust – a type A is a “subtype” of a type B when `a: A` implies that `a: B`. In this sense, `!` is only a subtype of itself; although it coerces to any other type, it’s not _literally_ of that type, the coercion is just invisible in syntax. Importantly, if A is a subtype of B then `Vec<A>` is a subtype of `Vec<B>` – but `Vec<!>` is definitely not a subtype of `Vec<T>`, since they may have totally different layouts in memory (the former not allocating at all, while the latter potentially allocating).
- kccqzy 4d ago> A is a subtype of B then `Vec<A>` is a subtype of `Vec<B>` That’s just not true. Java would permit it but then you get ArrayStoreException so this is unsound from a type system perspective. To make this sound, we need to classify each use of a type parameter to be covariant, contravariant, or invariant.
- jkhdigital 4d agoJava doesn’t permit that. You must specify covariant or contravariant type parameters with <? extends T> or <? super T>.
- kccqzy 3d agoThat doesn’t apply to plain old arrays, which were in the language before the designers actually collaborated with type theory experts.
- i2talics 4d agoYou are misled for two reasons. First of all, Rust isn't subject to the same soundness issue as Java precisely because of the Rust's ownership semantics. You can't produce the ArrayStoreException issue because you can't mutably alias a Vec in the first place. To be more precise, &mut T is invariant, but Vec<T> is covariant (in T). Second of all, Rust already does classify the co/contravariant status of all of type parameters. If you've ever tried to omit a type parameter from the fields of a struct and find that you're forced to insert a "PhantomData" value, this is because the entire purpose of PhantomData is to imply what variance classification the compiler should give the type parameter.
- echelon 4d agoWe should be able to set at a crate level whether our code can compile with panics, implicit conversions, etc. And we should be able to blacklist dependencies and transitive dependencies that do these things. We should be able to advertise a crate's safety and attention to detail. Higher level application code can benefit from this, but core libraries should forbid this statically and be prevented from even compiling or being imported should these things be enabled. We should be able to filter crates.io by these properties, and force our own projects to abide by them. I want nopanic, nocoerscion, maxdependencydepth, rustonly, nolinking, etc. flags.
- kibwen 4d agoDo you have some specific coercion in mind that you want to forbid? Unlike C, Rust is extremely tame when it comes to coercions. Forbidding coercions in general in Rust code doesn't really make sense, and I can't think of any that aren't either beneficial at best or benign at worst.
- cipherjim 4d agoWith ! the implicit conversion happens at compile time, never at runtime. It cannot by definition happen at runtime because the never type has no values and thus cannot be constructed under any circumstances. Any compile time coercions that occur would convert types (or generics args of types) to ! I find it difficult to imagine any situation where that would result in a working program - only if the coerced types or references to coerced generics were not even used would it compile.