Result<!, !> is an uninhabited type because both variants are; the compiler should be able to infer this and implement Uninhabited for Result<!, !>.
Re. use cases of being generic on uninhabited types... For example, proptest has impl<A: Arbitrary> Arbitrary for Option<A>. But we could also define impl<A: Uninhabited> Arbitrary for Option<A>.
Iâve meant that in you case always_ok and always_err will be available for Result<!, !>, while in my proposal without proving that Ok variant is inhabited you will not be able to use always_ok. Practically speaking itâs not a big difference, as getting value of Result<!, !> is UB, but I believe stronger bounds will result in a more robust code.
As for Option<!>, IIUC it is not an uninhabited type, but simply a ZST.
If I understood your examples correctly, you donât work with uninhabited types. In this case Inhabited trait approach will not change anything for you. Your code will stay absolutely the same. It will just add implicit protection against using uninhabited types, which can leak to your uninitialized calls, which in turn will lead to UB. So if anything, your code will become more robust.
Option<!> is not an uninhabited type, but ! and ParseError are uninhabited. By quantifying over A: Uninhabited I can provide this API for all uninhabited types.
Not sure about the "stronger bounds" bit... how is it stronger?
In my example we have two bounds on T and E, while in your example there is only one bound on E.
UPD: Can we use !Inhabited as a special naming for Uninhabited from your proposal? In other words can't we introduce !Inhabited without Inhabited and ?Inhabited as a first step?
My bad, youâre right. My understanding now is that the prior proposal (Inhabited trait) keeps mem::uninitialized intact, but applies the trait bound to it. That sounds great to me, and I appreciate you making this topic to defend that approach in light of the current proposal in the RFC.
because it raised some questions in a corner of the type system (uninhabited types)
seems to suggest that uninhabited types are not something you even think about when calling std::mem::uninitialized. If that is the case, this is a problem, because it's one of the things you must think about to know that your call is safe!
For instance, something like this could be considered part of what is "just an algorithm:"
pub fn example<T>(vs: Vec<Option<T>>) {
// Turn a Vec<Option<T>> into a Vec<T> and a Vec<bool>
let valid_mask = vs.iter().map(Option::is_some);
let values = vs.into_iter().map(|v| v.unwrap_or_else(unsafe { uninitialized() }));
// do something to `values`, in positions where `valid_mask[i]` is true,
// e.g. `std::ptr::read` them out to somewhere, then `values.set_len(0)`
// to leak all of the original copies
}
A naive author might think this code is safe because it only reads initialized values; but it invokes undefined behavior if called with vec![None::<!>], and the user is in control of T, so it is actually unsound.
Fortunately, OctreeNode is always inhabited, so this one call doesn't seem too dangerous in that respect. However, I am bothered by the fact that there seems to be an awful lot happening while the uninitialized data is live; too much for me to easily audit it for panics (which are a problem because if it panicked, you would have a drop of an uninitialized Vec).
That doesn't feel particularly stronger to me. If we have Result<!, !>, then .always_ok() is typed at !, which is fine, because we are never able to reach that point since anything that produces Result<!, !> must either diverge or is UB.
I'm uncomfortable with ?Inhabited implying that an Inhabited bound is default-assumed;
I'm also uncomfortable with assuming negative bounds and / or mutually exclusive traits will be a thing.
Instead, it seems to me that we should go with Uninhabited, and iff we decide to introduce negative bounds at some point, we can just say:
This is the normal way to define Void in Haskell. Since Rust marks Box as an âexclusive owner,â I donât think youâre allowed to use unsafe to construct a Box cycle, so itâs uninhabited in Rust, too.
So should it be considered uninhabited according to the Inhabited trait? I donât think Rustâs inhabited checker currently recognizes it.
I don't see this as valuable because inhabitedness is such a minor part of the potential problems you can hit with transmute. transmute::<(u16, u8), u32> is unsound too, as indeed are most combinations, so extra work on one particular small part seems unimportant.
(And transmute::<!, Void> and such are totally fine, so restricting both sides to be :Inhabited is overly aggressive anyway.)
That's a great reason to not expose inhabitedness in the type system, actually, since it'd be a breaking change to improve what it can detect.
That doesn't seem right...
The usual way to define Void in Haskell is:
{-# LANGUAGE LambdaCase, EmptyCase #-}
data Void
absurd :: Void -> a
absurd = \case
vacuous :: Functor f => f Void -> f a
vacuous = fmap absurd
How bad would the breakage be?
Generally speaking, any functor of an uninhabited type is also an uninhabited type per vacuous but getting the type system to understand this seems highly non-trivial.
Response to usage of memory operations + specifics of code example. We can continue discuss offline if youâd like â I donât really want to derail the current proposal.
I donât think this is black and white situation on multiple levels
The decision on whether or not to handle uninhabited types
I believe that handling uninhabited types is a design decision like any other decision. I (implicitly, from ignorance) decided that I wouldnât handle uninhabited types in my API, and my read of the situation is that users should continue to be able to make that decision. For me this is a similar issue to deciding how rigorously you check your preconditions in a method and how you fail. I can write a function as part of a repository fetcher that:
Barfs if you provide a malformed URL
Panics with a message
Returns a Err(âyou messed upâ).
I suspect that many users would want their code to barf on an uninhabitable type â they wouldnât want to carefully guard against it everywhere. Rust developers make tradeoffs like these every day. Things like
Should I even think about this potential error?
Should I catch and panic on this error?
Should this be an assertion to prevent the error?
Should I encode this error my api as a Result?
Footnote on that â Iâm totally fine with the middle ground of adding a trait bound on mem::uninitialized (though Iâll quietly hate having to propagate the trait annotation through all of my types). I understand the current RFC to suggest to use a wrapper type though, which I find pretty unpalatable. That said, itâd just be another on a list of things Iâm going to get cut by when the guillotine falls for Rust 2018.
The decision on how complex to let an unsafe block get
This is a judgment call as well. I actually write code in a vacuum (heh, pun on repo name) and generally expect that developers messing with data structure code, low level graphics code, or FFI code, be willing to sit and digest the unsafe stuff. I understand that this is a question of problem domain, code style, and familiarity with this unsafe code though. The Rust zeitgeist might tell you that there are hard positions to be taken (e.g. explicitly outlining your invariants on every unsafe block, discouraging unsafe blocks) but I resist them .
Surprisingly enough, mem::uninitialized (and even mem::zeroed) are dangerous to use even with inhabited types. Thatâs why we are trying to stabilize MaybeUninitialized.
For example, references (&T) are not allowed to be null, so calling mem::zeroed::<&u32>() is pretty much instant UB and will cause your program to randomly break. If you just call mem::zeroed::<StructThatContainsAReference>(), your program might not randomly break now, but optimizer changes might cause it to break just as well. This is of course still a problem with uninitialized values, as they might end up being all zero.
If we reach a âstatus quoâ position where we give let x : &u32 = mem::uninitialized(); a well-defined meaning (which I think we should as long as mem::uninitialized exists, given that this pattern is very common) then we can just as well give let x : T = mem::uninitialized(); the same meaning for T = !.
I think that this concern eliminates most of the necessity for an Inhabited trait.
I was under the impression that mem::uninitialized, mem::zeroed and mem::transmute have been considered error-prone/footgun-y/overly powerful APIs for a very long time (see arielb1âs post; none of those examples are new), and sometime around the introduction of unions it became clear that MaybeUninit and ManuallyDrop and similar types were just all-around better abstractions for most of the same use cases.
Therefore, I thought all this stuff about ! stabilization hitting a snag because uninhabited types conflict with mem::uninitialized and friends was merely a reason to speed up deprecating them, not the sole or primary reason we want to deprecate them in the first place.
I think this is the third or fourth thread Iâve seen that started from the premise that âtheyâre getting rid of mem::uninitialized just because of uninhabited types, and I disagree because âŚâ and I still have no idea why so many people think that was the sole motivation.
As you can see some users completely unaware about pitfalls associated with uninhabited types and write unsafe code accordingly, so requiring explicit opt-in will be a great fail-safe. Honestly I am more uncomfortable with the current status quo and I believe that MaybeUninit is a fig leaf, the only useful thing about which is ability to decouple uninitialized variable creation, writing into it and "converting" into desired type. And because creation of uninitialized variable and work on it usually tightly coupled I don't see that much benefit in it. All pitfalls are still here, we just added one small step and hope that it will protect users from uninitialized pitfalls.
Can you elaborate why you and @hanna-kruppe wary about negative trait bounds?
I believe it's very much inhabited type. (note that it compiles, and segfaults only at runtime) Essentially you've just created a recursive reference into heap. I think rule of the thumb here is: if mem::size_of does not return 0 it is inhabited type. (well, if we exclude "pathological types" like (u32, !), which I believe should be forbidden)
As I've wrote earlier this proposal is not just and not as much about safety of transmute.
Can you provide a real use-case for such transmute call? Considering that with E=!:
match result { Ok(v) => { .. }, Err(e) => { .. } }
In error branch e will be considered inhabited type?
See explanation earlier, IIUC the provided example has nothing to do with uninhabited types.
As I've wrote in the beginning of this post, I don't think that MaybeUninitialized brings too much to the table, you just shift uninhabited type creation to the moment of flipping union to value variant. All hazard of generic code are still here, you've just covered it a bit. Yes, we can move unsafe block around, but dragons are still around the corner!
My understanding was that working with uninitialized memory always has proverbial dragons, the best we can hope to do is make the unsafe code easier to write correctly and see that it's correct when reading, and the MaybeUninit abstraction is less error-prone precisely because it moves the unsafe blocks to the right place to achieve that. So... I guess I technically agree with what you said?
(I'm aware of proposals like &out references, but afaik those don't cover all cases, just some of the most common ones)
Could you link whatever this was more explicitly? I've just reskimmed the entire thread and didn't see any useful examples of code that would benefit from an Inhabited trait, so I'm still missing what the motivation is supposed to be.
Meta note: I feel like ^this for a LOT of threads on this forum lately. We need to spend way more time on gathering and dissecting use cases and way less on debating these ideas in the abstract.
I'm concerned about making the assumption itself more than about negative trait bounds.
What if we don't end up adding negative trait bounds?
The segfault here implies that you've not constructed a valid instance of the type. This is thus not a valid example. If you somehow manage to construct a valid Haskal using safe Rust, that should amount to a soundness hole in the type system.
I was talking about notriddle's Haskal example, which you've used as a false example of "breakage". For examples see e.g. this post about Result. Also !Inhabited could be used to explicitly require uninhabited type in generic contexts. (I think there was a thread with request of the similar feature)