Killing `Ok(())` without ok-wrapping

I have no idea whether this is a good idea, but it just popped into my head, especially w.r.t. Termination being stable.

We add a new trait (all names are silly placeholders):

pub trait Implicit {
    fn from_nothing_something() -> Self;
}

impl Implicit for () {
    fn from_nothing_something() {}
}

impl<E> Implicit for Result<(), E> {
    fn from_nothing_something() { Ok(()) }
}

Whenever a block ends with the empty expression (i.e. an implicit () value is added as the block result), the block instead returns Implicit::from_nothing_something(), with a unbound type fallback to ()[1].

This would mean that you could write something like

fn main() -> eyre::Exit {
    jane_eyre::init()?;
    init_tracing()?;
    do_main()?;
    thank_you_for_playing_wing_commander()?;
}

and then behavior of program termination would be determined by eyre::Exit::report(AppError.into()) in the failure case, and eyre::Exit::report(from_nothing_something()) in the success case.

Notably,

fn do_stuff() {}

fn main() -> eyre::Exit {
    do_stuff //~ ERROR: mismatched types
}

Implicit would only be used in the case where a block ends with a statement and not an expression.

(... and in the case of if {} else {} which is conditionally an expression or a statement based on tyck ... :person_shrugging:)


  1. This fallback behavior is the same as is used by {integer} and {float} literals. This fallback is not available to be used for anything other than these two pseudotypes, so this necessarily makes this feature a far-future kind of thing blocked on making type fallback more available; e.g. work on custom literals also requires this. ā†©ļøŽ

11 Likes

I'm not sure how to feel about this. I would love implicit Ok(()) (and implicit Some(()) for what it's worth), but I'm not sure allowing implicit anything is the right way to go about it.

13 Likes

The same here, tbh. With some sort of try fn, though, it's still "implicit anything," just via impl Try<Output=()> rather than impl Implicit. Where possible, my personal design philosophy prefers making a set of types a trait rather than a language built-in.

1 Like

I disagree. It's not implicit because the try is there.

let _: Option<()> = {}; is implicit.
let _: Option<()> = try {}; is explicit.

And the same would apply to something function-level.

7 Likes

First, that sounds like a recipe for a performance disaster.

Second, this is likely to cause inference problems like with try, unless we limit it to returning from functions.

Third, what with explicit return?

Nitpick: you probably need to change this to:

impl Implicit for () {
    fn from_nothing_something() { () }
}

because the original implementation recursively calls itself!

14 Likes

I really like that this enables us to write it like this.

however it also makes it very difficult to argue about more complex expressions:

With try blocks, we can write this:

#![feature(try_blocks)]

pub fn foo(input: &str) -> Result<Result<(), ()>, Result<(), ()>> {
    match input {
        "" => try { try {} },
        "a" | "b" => {
            try {
                bar().map_err(|_| try {})?;
                try {}
            }
        }
        "wow" | "world" => {
            try {
                try {
                    let res = try {};
                    if input == "world" {
                        return try { res };
                    }
                    res?;
                }
            }
        }
        _ => todo!(),
    }
}

pub fn bar() -> Result<(), ()> {
    try {}
}

And combining that with this feature would yield:

pub fn foo(input: &str) -> Result<Result<(), ()>, Result<(), ()>> {
    match input {
        "" => try {},
        "a" | "b" => {
            try {
                bar().map_err(|_| {})?;
            }
        }
        "wow" | "world" => {
            try {
                try {
                    let res = {};
                    if input == "world" {
                        return try { res };
                    }
                    res?;
                }
            }
        }
        _ => todo!(),
    }
}

pub fn bar() -> Result<(), ()> {}

I especially dislike this part:

let res = {};
if input == "world" {
    return try { res };
}

If there is greater distance between creation and return of res, then it becomes very difficult to know the type of res without the help of an editor equipped with an lsp.

I feel like we could get the same nice looking code, if we allowed this:

fn main() -> Result<(), ...> try {
    jane_eyre::init()?;
    init_tracing()?;
    do_main()?;
    thank_you_for_playing_wing_commander()?;
}

(I was not able to find any eyre::Exit, so I changed it to Result)

So allow using try blocks at every normal block position. This way we avoid the additional indentation by using an extra try block and still have the normal try behavior.

1 Like

Personally I greatly prefer explicit Ok, though I don't love wrapping (). I kinda think I wish ZSTs that only have one possible value could just use _. i.e. return Ok(_);.

9 Likes

:thinking:

hmmā€¦ā€¦ now this somehow got me into writing this code:

trait Okay {
    const OKAY: Self;
}

const fn ok<T: Okay>() -> T {
    T::OKAY
}

impl Okay for () {
    const OKAY: Self = ();
}

impl<T: Okay> Okay for Option<T> {
    const OKAY: Self = Some(ok());
}

impl<T: Okay, E> Okay for Result<T, E> {
    const OKAY: Self = Ok(ok());
}
#![feature(try_blocks)]
pub fn foo(input: &str) -> Result<Result<(), ()>, Result<(), ()>> {
    match input {
        "" => ok(),
        "a" | "b" => {
            try {
                bar().map_err(|_| ok())?;
                ok()
            }
        }
        "wow" | "world" => {
            try {
                try {
                    let res = ok();
                    if input == "world" {
                        return try { res };
                    }
                    res?;
                    ok() // optional, in case you like itā€¦
                }
            }
        }
        _ => todo!(),
    }
}

pub fn bar() -> Result<(), ()> {
    ok()
}

Rust Playground


I guess, Iā€™m kind-of making three points at hereā€¦

  • the trait could use an associated const instead of a method, this way also fewer ā€œsurprisingā€ things could happen (and it works in const fn)
  • the thing could work for nested Result/Option, too
  • really, I feel like ok() is already a lot cleaner than Ok(()); the double-parentheses seems quite annoying. I dislike the magicalness of {} as a value in the code example, so an explicit call like ok() seems like a potentially reasonable compromise
  • (also, this is possible on stable, and as a library, and if it should become even shorter, then a parentheses-less keyword-based solution would be yet-another-option)
6 Likes

To be clear, it was a hypothetical impl Try + Termination type, not an existing one. I would not be surprised to see such pop up in application error frameworks, and they can't just be the existing Result<(), ApplicationError> because Result<impl Termination, impl Debug> is already Termination.

Would it be an option here to instead special case (generic) enum variants containing () to allow to omit the () (and possibly even the wrapping parentheses too)?

IIUC, this should be unambiguous and relatively clear:

fn bar() -> Result<(), ()> {
    Ok()
}
// alternatively
fn bar() -> Result<(), ()> {
    Ok
}
2 Likes

Ok as a value already has a meaning, it's a T -> Result<T, E> function.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e8527ded6a40e69e9455b1d8a786831d

let _: Vec<Result<u32, ()>> = (0..10).map(Ok).collect();
4 Likes

Oh right. Does Ok() also already have a meaning, or would it work?

It doesn't have a meaning, it will currently error that too few arguments are provided. Given that Ok is essentially considered just a function, allowing Ok() feels a bit like I introducing a variadic function (allowing 1 or 0 arguments). Currently something like variadic functions is only possible on nightly by manually implementing both Fn(T) -> ā€¦ and Fn() -> ā€¦ for a type.

3 Likes

To be honest, at this point it may be worthwhile to have a proposal solely for implicit Ok(()), with the mechanism unspecified. Experimentation could then happen on nightly as to the best way to implement it.

Reminder that this (closed, albeit postponed) RFC exists, from 2017:

Personally I think that something involving try is the way to go, though.

4 Likes

It's a bit of a visual wart, but alternatives I've seen mentioned here (including implicit Ok(()) only seem to add complexity rather than take it away:

  • implicit Ok(()) means an extra thing to remember if the return expr is to make sense vis a vis the declared fn return type

  • Options based on control flow such as try {} may cater to specific use cases and thus have merit in general, but they only seem to make code more complex when considering Ok(()) in isolation.

Could someone explain to me the desire to annihilate this?

9 Likes

Ok(()) is mainly just noise to satisfy the type checker. In theory, Implicit should just be used for Try::from_output(()) like thingsĀ¹, where it's just ceremony to say that yeah, we're done here.

(I honestly kind of hate that I don't have a better way to explain this right now but) try is basically our do for monadic execution. Because ? works without a try container, it's not necessary to use try at the function level, but it makes return/yeet do the correct monadic thing, meaning my example would be written (using an arbitrarily bad try fn syntax)

fn main() -> try eyre::Exit {
    jane_eyre::init()?;
    init_tracing()?;
    do_main()?;
    thank_you_for_playing_wing_commander()?;
}

Ā¹: This gives another idea for trait Implicit: just trait Implicit: Try {}, and it opts in to Try::from_success(()) at the end of a normal block, again only if an implicit () return is used. I don't really thing it's a good idea candidate anymore, but it is a possibility.

2 Likes

What's people's gut check for _ in r-value position being a shorthand for (say) Default::default()?

Given how it's used today, it would be confusing I think.

1 Like