What about a `default` function modificator

I started noticing that functions that return Result<(), _> are quite common, and most of the time they end up with a Ok(()), which feels heavy. I mean, there is the try! macro with its very convenient ? shortcut which makes using Result so painless, but at the end you have to add something that feels like the compiler could have figured out alone (or that is used so often that it could even be a special case). I also saw some discussions about it, where some people argued for making it a special case (ie. writing nothing and the compiler guesses the Ok(()) on its own), and some arguing against it saying that, in Rust, most of the things are explicit, and that this is a good thing.

Tangentially, I also read about the proposal to make Default derivable for enums. And that's when I got an idea. Wouldn't it be possible to add a "default function modificator" (the name isn't very good but I couldn't figure out something better), similar to the visibility keyword, that would work as following: given a type T such that T: Default, and I am writing a function func such that func: fn(_) -> T, then I could add the default (or maybe just def?) keyword before fn:

default fn func(_) -> T {
   ...
}

such that, for each branch of func where nothing is returned, an implicit T::default() is returned instead.

This would be quite convenient, while still making it explicit and, very importantly, not enabled "by default" for every function, since that could screw up silently someone's code (ie. they forgot to return somewhere, and if that feature would be enabled for every function, the compiler would silently accept it and behave in a way that was not what the user expected).

This would also avoid creating a special case only for Ok(()), which is good because nobody likes special cases, and everybody likes meaningful language features that are useful in lots of places.

This seems confusing to me. There's already an (unstable) feature related to putting default on functions, and it does something entirely different.

Besides, I don't see the point in introducing new syntax for such a minor inconvenience.

3 Likes

I'm personally happy to write 6 extra characters if it means the language doesn't have yet another mechanic or special case that then needs to be taught to people.

I reckon we should only try to introduce something into the language if it unlocks new capabilities that were previously impossible (e.g. const functions), makes something orders of magnitude easier to use (e.g. async/await, const generics), or reduces the number of gotchas in the language (e.g. deprecating trait objects without the dyn keywords).

Also, not to be pedantic, but default is 7 characters while Ok(()) is only 6. So that hampers the original "avoid extra typing" premise ... Although I'm guessing we would call it try instead of default, so it might not be that bad.

The Rust language authors really don't want the compiler to be guessing what developers want - other than type inference, you'll find that pretty much everything is explicit in Rust. Even something as simple as copying a struct (done implicitly in languages like C or C++) requires either a clone() call or opting into copy semantics with the Copy trait.

This implicit-ness means that almost every decision a developer makes can be tied back to a specific part of the source code, which in turn makes it easier to reason about the code or pinpoint weird performance issues or compilation errors.

8 Likes

In most cases it makes sense to structure your functions so that there's only one "default" happy path out – if you find yourself using several Ok(()) per function, perhaps the function could use a refactoring.

There's been talk about adding shortcut function such as std::prelude::default() (or even …::def()) which may be useful if Ok(()) looks too line-noisy, and is of course useful in several other contexts where currently you have to write Default::default() or YourType::default().

Indeed, this would rule out this new syntax.

I feel that this feature (whatever form might it take) goes in the direction of adding language-level support for Result manipulation. Think of ?: it's just slightly shorter to write than try!, it's a special case (you replace a macro call that everyone understands with a brand new syntax element that you must have read about to understand it) and it doesn't create new possibilities. But it just makes the code more elegant. It's not necessarily about writing less characters overall.

Yes, that's why with this feature you'd have to opt-in to use it, and the compiler wouldn't have to guess anything.


I think that, overall, your arguments are very convincing, but I still feel that the current situation could be improved...

Language feature requests are off-topic for this forum, use IRLO for that purpose. Anyway, this is nothing new, and it has come up and been denied several times, so don't expect it to be accepted this time, either. Basically, the need for typing Ok(()) is not a design error in the language (you have to type out every other value, too), so it's not something we should be getting rid of. Not having to write 6 characters is a very weak motivation for changing the core language itself.

But the great thing about Result is exactly that it's just a normal enum. A type system is good not when it has special cases baked into the language for every type, but when it's expressive enough so that we can express many different things with a few general features.

For one, I was opposed to a custom operator just for fallible types; I still don't think it adds much, and I don't see why it would be that much more "elegant" than try!, either. If anything, doing the job with a macro had been more elegant, because that's using an already-existing, general feature to accomplish the same goal as that of the new, shiny special operator.

3 Likes