Language Tidy Up - Feature #2 - implicit into() on return

If there is a datatype being returned and the return type has a from trait implemented for it for that return type, then the compiler will automatically add-in that "into()" method call for you.

It might not handle all situations, but should help a lot in terms of readability in the cases where it can be done (from my personal perspective). It doesn't have to be haskell concise, but it's pretty verbose right now.

Personally I prefer such conversion to be explicit. from/into can be expensive (e.g., &[T] to Arc<[T]> requires cloning all the elements). Even if they weren't, explicit is better than implicit ~99% of the time IMO.

20 Likes

If you need to call into() on return, you can just... call .into() on return? It's not a common enough problem to warrant hidden transformations, and it will compose poorly with generic code, or type inference in closures.

11 Likes

Remember that if the return value was always .into()ed, that means that fn foo() -> u8 { 0 } wouldn't compile any more.

Just write .into() when you need it. It's really not a big deal.

8 Likes

I feel like the opposite, a hidden function calls makes readibility worse because now you have to ask yourself if there's some kind of hidden conversion going on.

1 Like

Counterpoint: we have an implicit From::from in the translation of ?'s return path. What's different between yeet and return that yeet should do an implicit conversion but return shouldn't?

Should <Result<T, E> as Try>::from_output accept any Into<T> instead of just T? This would make the type conversion done by the Try implementation uniform[1] between the Break and Continue cases, and not impact return from a normal function.

(Adding a blanket conversion for Result's Ok type when Into::intoing the whole is breaking anyway, so the non-try and the try case are fairly disjoint.)

I'm quite split here; at a gut level I agree that introducing an implicit conversion here isn't desirable, but I'm having a hard time articulating why I'm perfectly fine with the implicit conversion for Result's Err variant.

Perhaps it's how much it saves? ? doing conversion saves multiple instances of .map_err(Into::into) per function, whereas return doing conversion saves a single .into() per return Ok(_), which are much rarer (typically only the tail return, even).

Perhaps it's convention? Result's Err type conventionally implements Error and only implements From for wrapping a source error, and typically only heap allocates the source error if its an "unexpected" errors likely to just be yote up the stack where stack size overhead is more costly to program execution than a presumed cold allocation.

Perhaps it's familiarity? IIRC try! was added essentially the version after I started using Rust, ? has been available longer than not, and the From::from conversion of the yote error has been there from the beginning. On the flip side, a lack of implicit conversions in normal program execution is a selling point of Rust over C++, though it's not as big of a focus with the shift in marketing focus from C++ domains to more general development empowerment (notably application development, not just systems development, for all the good that distinction is(n't) worth).


Separately, I think a significant chunk of want for an implicit conversion on return is probably better served by more specific features which provide coercions, e.g. enum dyn Trait[2] or other anonymous-enum-adjacent features would likely coerce from their constituent types, removing the needed .into() from using -> Box<dyn Trait> instead.


  1. modulo the very unfortunate legacy and type inference reason ? uses From instead of Into ↩︎

  2. More commonly enum impl Trait, but I have a weak preference for spelling it dyn Trait to emphasize the reliance on object safety, even if the implementation uses enum dispatch rather than dynamic dispatch. In practice, probably both should be offered depending on if you use enum impl Trait or enum dyn Trait ↩︎

6 Likes

I don't think that's really a counterpoint. The From::from conversion on ? is something which regularly causes me frustration. It means that I can never use it in a closure without explicitly specifying its error type, even if all returned errors have the same type, even if there is a single fallible function call. It's also a significant part of why try blocks are still not stabilized: using them with explicit error types feels too burdensome and not really fitting syntactically, but implicit error conversion makes them impossible to use otherwise. I also slowly drift towards the opinion that silent conversion of errors to a single type via ? is an antipattern: it leads to having either big-ball-o'mud toplevel error enums, or to Box<dyn Error> everywhere. In reality, you want finer-grained control over error propagation, and you usually need to include some extra context via map_err() or context() call anyway. Otherwise the errors become useless when passed through several layers of call hierarchy: file not found - where? why?

If we choose between "make return do automatic conversion" or "no implicit conversion at all", then I'd rather choose the latter, because I end up doing those conversions most of the time anyway. We could also introduce a shorter combinator for the common map_err(Into::into) case, like TryFutureExt::err_into in the futures crate.

At the same time, I consider the current implicitly converting design of ? to be the right decision. The reason is that the real-world competition isn't between automatic and manual conversion of errors. The real competition is between ? and .unwrap(), and the former is definitely a better choice. Fact of the matter is that proper error handling is usually an afterthought. People focus on the happy path, and treat all deviations as irrelevant exceptions. I am guilty of this myself. I know that I shouldn't do it, but proper error handling is hard, and there is so much pressure to have the happy path implemented. If proper propagation of errors would look like

x.map_err(|err| error_classifier(err))?

then most people would never use it, they'd just write the much shorter x.unwrap(), crash the thread and complain about Rust's verbose error handling (which they already do).

The analogue of ? in other languages is exceptions, which also effectively do implicit casts (by casting to supertypes and subtypes). Most people are fine with it most of the time. In plenty of cases it really is good enough. If Rust's error handling model was to be viable, it should have been just as easy to use. The current situation is mostly ok: as a first-order approximation, you can make a ball-of-mud enum of toplevel errors and convert all specific errors into it. That's it, you're done. Writing the business logic is now just as easy as in exception-based languages, but it is more robust, and it's easier to check for specific errors when they happen. While you may want a better error API eventually, you can introduce it gradually via small-scoped refactorings, without major changes of your code (like introducing new catch_unwind blocks).

3 Likes

(Hopefully being addressed soon™. Sneak preview: rfcs/0000-resolving-try-annotations.md at try-again · scottmcm/rfcs · GitHub )

12 Likes

Those people complaining about yeet are really going to hate , but it's so tempting to keep that while unstable...

6 Likes

Personally it's exactly because ? is meant to deal with errors and options (which for the purposes of this discussion, can represent anonymous errors with their None variant), coupled with the fact that often when they are returned, it is desirable to convert their type eg a module-level Error type to a crate-level one.

This behavior is not at all desirable for values in general; error values are somewhat special in that regard.

1 Like

Out of topic: You just made me realize that there's no syntactic ambiguity by making it just enum Trait:

fn foo(x: enum Trait) -> enum Trait { todo!() }
type Alias = enum Trait;

of course, this could be confusing, so probably best not to go with it. /out of topic

2 Likes