Just a pondering.on result<(),T>

I’d say Err being the happier response is a rare case.

That's not what I'm saying... I'm saying that Err is not the 'happier response', it is just a result. The difference is manifest when you use foreign code: if you wrote a fn returning Result, and you're saying Ok represents the "happy path", but I write a program which is designed to efficiently handle your Err path, then Ok is no longer the happy path. Which path is 'happy' is entirely dependent on what code you're writing.

Using the FromNothing trait idea would not stop anyone from specifically writing OK(()) where they felt it fit.

This is where you get inconsistencies. Some functions will return what they say, and others will do a silent conversion. The only way to know what is happening is to compare the returned value with the signature, and also to know that Ok and () have special rules that don't apply to other types. You don't have any of these problems with ?, so that comparison isn't indicative of the value.

1 Like

There’s no silent conversion between types with the FromNothing trait.

if the signature return type implements FromNothing && the coder hasn’t specified a return" { return T.from_nothing() }

Not for closures, or macro generated functions. It might also be further away.

2 Likes

So you’re saying you want to silently insert a call to a conversion function, but that this isn’t a silent conversion?

It’s not conversion; it’s creation. There was no object not even a () because this would replace that.

This is type inference.

Well, now you’re just playing word games.

2 Likes

not really:

pub trait FromNothing{
    fn from_nothing()->Self;
}

if it doesn’t take an input it’s not a conversion.

In any case, you’re entirely missing my point. One shouldn’t have to guess at the relevance of the function signature to know what the code is doing.

Consider how ? works: When you see this in your code, you know immediately that the return type of the function dictates what happens. With your proposal, you have to do some detective work and reason it out. You’re doing the opposite: you’re taking the Ok signal and eliminating it so that people have to guess as to whether the returning statement or the function signature is specifying the end type.

1 Like

The bigger problem now I see is the closures. As if the language used this trait instead of () it would be harde to pick a sane default

– for places where the return type is implied.

Rust already have several features which counter your statement:

  • type inference
  • implicit conversion in ?
  • async fn

try fn is a logical evolution of existing features and try blocks.

1 Like

The fact that one has to doesn't counter the opinion that one shouldn't. It should be decided on a case-by-case basis. In my opinion, code being less dependent on other (possibly far away) code to be understood is a good general quality.

2 Likes

I agree about deciding on a case-by-case basis. My point was that the cited principle shouldn’t be perceived as an axiom which should be upheld above everything else.

1 Like

The principle I stated was not that one should not have to look at the function signature, but that one should know whether or not they should look at the function signature. For instance, with ?, From::from is always called, so you always know the signature is relevant. With from_nothing et al, you only need to look at the signature if it happens to contain Result<(), T>

3 Likes

Ah, then I agree with you.

Oh, there absolutely are happy and unhappy paths for Result<T, E> and no, Ok and Err are not symmetrical. Everything about how the type Result<T, E> is set up says that there's a happy path on which you map, flat-map, and so on.

This is also the case in Haskell where Either e a is not just your run of the mill coproduct.

data Either e a = Left e | Right a

instance Monad (Either e) where
    return = Right
    Left e >>= _ = Left e
    Right a >>= f = f a

Everything about how Monad is defined here says that Left and Right are not equal data constructors.

To my knowledge there's no usage of From::from when impl<T> Try for Option<T> is used. It generally is unlikely to hold that From is relevant for all impl Try types.

The same methods exist for the Err case, so I'm not exactly sure what you're saying? map and map_err are symmetrical, and_then and or_else are symmetrical, etc. Outside of that, Result<T, E> is symmetrical with Result<E, T>, whichever you prefer being a matter of convention or convenience. Neither of them allow you to produce code that isn't just a rote isomorphism away from the other, as far as I can tell.

To my knowledge there’s no usage of From::from when impl<T> Try for Option<T> is used.

Touché. (I was extrapolating from the try! macro.) But still, ? always asks you to look at the function signature, as it's purpose is to signal that a return and conversion might be occurring.

  • Their naming is not symmetrical; you don't have map_ok.

  • There is no iter_err() to correspond to .iter() (same for iter_mut).

  • also: Result in std::result - Rust has no corresponding impl for E.

You can't pretend away ?; the way it behaves for Result<T, E> blesses Ok and not Err as the happy path.

I think it is quite clear that Result<T, E> is imbued with semantic meaning in the operations it has. It is not just a dumb coproduct, it is about reified exceptions ("errors as values").

Or try { ... } if it is used.

I'm a proponent of try fn myself which tells you about the behavior in the signature.

Their naming is not symmetrical; you don’t have map_ok .

If you pick a topology where no two points are connected, then you're not going to find much value in classifying things based on topology. Surely naming conventions are not a relevant notion of computational symmetry?

There is no iter_err() to correspond to .iter() (same for iter_mut ).

This may as well be a bug. There's nothing about Result that makes this an invalid operation. It is only adherence to convention that prevents it from existing. (More importantly, the existance of this function would only be a concession to the symmetry between Result<T, E> and Result<E, T>; the symmetry is fundamental, so you can already recover the behavior of iter_err() by applying the isomorphism and calling .iter() on the result.)

also: https://doc.rust-lang.org/nightly/std/result/enum.Result.html#impl-Product<Result<U%2C%20E>> has no corresponding impl for E .

This impl exists: impl<E, U, T> Product<Result<U, T>> for Result<T, E> where E: Product<U>. Again, whether you use this impl or that one is a matter of convention.

You can’t pretend away ? ; the way it behaves for Result<T, E> blesses Ok and not Err as the happy path.

The ? operator blesses Ok as the easy-to-write path. Whether or not it is the 'good' or 'happy' path is context dependent.

I think it is quite clear that Result<T, E> is imbued with semantic meaning in the operations it has. It is not just a dumb coproduct, it is about reified exceptions

It is given a syntactic meaning... Semantically, it reifies a branch, and the code it generates makes no distinction between which branch is preferred over the other. ("exception" is too ill-defined to be encoded in the semantics of Result.)

Everything about Result that pushes you to prefer Ok over Err is artificial in the sense that is based in human expectations and conventions. The actual software that is produced doesn't change, and thus the logic that generates it is the same in either case.

1 Like

It's a quite strange way to look at things in my opinion. Types are not only about binary representation of valid states, they are also to a large extent about "expectations and conventions". Wrapper types are the most evident example here, when you see Meter(f32) you start to expect a lot of things from it and will write your code accordingly, even though it's "just" f32.

Result has a clear semantic meaning and we construct APIs and deal with it according to this meaning. Either while being structurally equivalent to Result has a very different meaning, and I highly doubt that most programmers will think that they are interchangeable.

2 Likes

I don't think you're reading what I wrote very clearly, or else I didn't communicate it properly. The structural similarity of Result<T, E> to Result<E, T> -- that is, the binary representation of those types -- is not what I'm concerned with.

I'm also not talking about the difference between Result<T, E> and Either<T, E>. I said that Ok and Err are symmetrical; that you could exchange one for the other (everywhere) and produce equivalent programs. You cannot produce equivalent programs by exhcanging Ok with Left, because Result and Either support different operations. (You could replace Left and Right, but that is not a distinguishing feature of Either.)

Meter(f32) you start to expect a lot of things from it and will write your code accordingly, even though it’s “just” f32 .

What Meter(f32) does for us is establish the validity of various operations on the type. It is not isomorphic to f32 in a logical sense, because f32 permits different operations. But this is not true of Result<T, E> and Result<E, T>: the exact same operations are permitted on either type (modulo their names and function composition). The differences between Ok and Err are only that the operations dealing with them are given different syntactical forms, à la the ? operator.

EDIT: I may be conflating some things improperly here, though. If Result defines ? to operate one way, what basis do I have to argue that this isn't an difference in operation, the same as any other... The ground may not be sturdy here. What I'm trying to express is that an isomorphism between Result<T, E> and Result<E, T> includes transformations for any methods defined on the input, but ? isn't defined the same way on the other side, so it shouldn't be an isomorphism then.

1 Like