Why no impl From<()> for NoneError

I have a function f(...) -> Result<Self, ()>. It is not possible to use Option here because of trait bounds.

I want to use f(...) inside a function that returns Option<T> by calling let x = f(...)?;

I get the following error message: "the trait std::convert::From<()> is not implemented for std::option::NoneError". Why is this trivial trait implementation not done? Conversion from () to NoneError should be trivial and the Conversion from Result<T, ()> to Option<T> seems quite common to me.

I know that this api is still experimental, but I am interested if there is a deliberate decision to skip this trait implementation.

Take a look at https://doc.rust-lang.org/std/result/enum.Result.html#method.ok

So I can write let x = f(...).ok()?; instead? This seems fine.

Still, is it a deliberate decision to not implement the trait From<()> for NoneError?

My understanding is NoneError's fate is still TBD (see this internals thread), and I suspect people didn’t want to give it more bells and whistles while it’s still an unstable type. Then, you have existing methods like ok() (and a few variants of it) that sort of make the conversions not very useful (IMO, at least).

It’s also slightly odd to me to convert () to NoneError - one is a value (just happens to be unit), the other is an error type but only really useful for Option's Try impl. So they’re kind of similar, if you squint and tilt your head sideways, but aren’t really the same type of thing.

But that’s just my opinion/take - someone from libs (@scottmcm?) can probably provide a better answer.

1 Like

@vitalyd I come from Haskell and for me, they are clearly the same thing: A type that has only one value. You seem to point at some difference. Could you elaborate on that? I’m interested in the view of the Rust community.

There are potentially infinite types that have only one value – like any struct Foo; – but that doesn’t mean that it’s semantically meaningful to convert between them.

2 Likes

I by no means speak for the Rust community, so take what I say with the appropriate grain of salt :slight_smile:.

While it’s true that () and NoneType are unit types (i.e. have a single value), they’re distinct types in terms of prescribed semantics. You can have two unit structs - they have no associated state and can be implemented as such - that each represent completely different things in the context of their usage.

What is an example of "represent different things in the context of their usage"? I guess, this was my real question. Also, why do () and NoneError represent different things in this case.

This is probably your real question?

I suppose one way to look at this would be: should every type out there (or even in std alone) that represents “nothing” be convertible to NoneType? () is but one example of such a type (and happens to be a language builtin).

But this gets a bit philosophical quickly. My feeeling is Rust’s stdlib maintainers didn’t want to “double down” on the NoneType, at least not yet (as that linked thread suggests).

In your case, I don’t think there’s much controversy about the conversion being fine and exactly what you need. In general code, it may also serve as a decent speedbump in ensuring no Result is accidentally lost via the (somewhat) easily missable ? operator.

I'd like to point out that all of the typelevel integers in typenum are isomorphic to each other. :duck:

This is precisely the word I would use to describe the NoneError situation.

1 Like

@ExpHP @vitalyd What has a speedbump to do with this all? Shouldn’t () and NoneError as well as any other unit type be compiled to the same thing? Also, the from-conversion should be a no-op as the physical representation is the same (if there even is a physical representation; I guess it is left out entirely)

As I said in the thread @vitalyd linked, I think that .ok()? is the correct way to mix result and option, even long-term.

But also, I consider Result<T, ()> to be an anti-pattern. One should always just make their own ZST, as that’s clearer to the caller and lets you have whatever impls you want.

1 Like

Hmm, I’m curious why you’re focusing on the physical/compiler level lowering of these things but (seemingly) glossing over the semantic aspects that the type conveys (if it didn’t convey anything, why does it exist?).

Both () and NoneType are known as ZSTs: zero sized types. This is a really cool (and useful) feature of Rust. Since they have no state, they can be used to attach type-level functionality. That is, you can use them to describe compile-time only aspects. Once compiled, they evaporate.

To make this a bit more concrete, there’s a pattern in Rust called session types (aka typestate). Here’s a (very) contrived example:

struct Open;
struct Closed;

struct Connection<T>(PhantomData<T>);

impl Connection<Open> {
    // close() only available on Connection<Open> type
    fn close(self) -> Connection<Closed> {...}
}

impl Connection<Closed> {
    // open() only available on Connection<Closed>
    fn open(self) -> Connection<Open> {...}
}

Open and Closed are both ZSTs, but they capture different meaning at the type level. In fact, they’re just type level “tags” used to denote the state of another type, Connection.

But even more generally:

// used as an error type when submitting work
// to a shut down threadpool
struct ThreadPoolShutdown;

// used to indicate that some signal was sent
// somewhere - unrelated to any threadpool
struct MySignalSent;

Both are unit types/ZST. But used in different places to convey entirely unrelated information.

I agree with this assessment.
Specifically, there are 2 types (unit aka (), and NoneError), that happen to have in common that they each only have 1 value. That does not make them the same type, or even compatible w.r.t. error semantics. While I agree it might make for some more ergonomic code here and there, I don't think it makes any semantic sense to create a NoneError from "nothing" ("nothing" because fns that have "no" return value actually do have one: ()).

Indeed. In addition, if you're using Result<T, ()> then you may as well switch to Option<T> as the intent is clearer.

Isn't that essentially the idea of a type-level state machine (where the states are Connection<Open> and Connection<Closed> in your example)?

It is, although you can use the same technique for things other than state machines (eg type level integers ala typenum). This blog goes into it in depth.

1 Like