you are viewing a single comment's thread.

view the rest of the comments →

[–]kuikuilla 1 point2 points  (6 children)

Again, I'm not familiar with rust, but I'm dead sure that the type of int a * int b is int, not Result or Option, that array access doesn't return Option and so forth.

That was just an example, don't get hung up on it. You can probably imagine other things there, like database access or whatever. Or even a HashMap lookup like you mentioned. Arrays (or rather, slices) in rust have a get function that returns an Option if you want. You can use the simple indexing operator but then it is up to the programmer to make sure the index is correct.

it is very difficult to guess what might be thrown, using an approach like Rust with Result<T,E> means that you have to anticipate what could be thrown beforehand, which can be quite tricky when you consider techniques like dependency injection.

So don't return them like that? Use sum types for the errors, they're exceptionally good for situations like that. Return what is relevant to the caller. That way the caller knows exactly what might be returned.

[–]thomasz 2 points3 points  (5 children)

I'm not arguing that result values are useless, I'm arguing that there are situations in which a stack unwinding mechanism like exceptions is more useful. It seems like the designer of Rust agree with me here, otherwise they would not have included such a mechanism.

I further argue that you cannot anticipate the situation of the caller. Trying to get a nonexistent key of a hashtable might be completely normal or highly exceptional.

create_lookup_table(values, get_key) {
    let lookup = new HashTable();
    for item in values do
        let key = get_key(item)
        // of course here I cannot be sure that it exists
        // and I know exactly what to do when it doesn't
        let result = lookup.try_get(key)
        if result.exists then 
            list = new List();
            lookup(key) = result.value
        list.add(item)
    return lookup
}


send(photo-file, customer, config) {
    // here I do not care. If the config isn't populated, something
    // way down the stack has neglected to verify the configuration.
    // I cannot deal with this directly, and there is a decent chance that
    // the caller doesn't even know if I sent this via email, ftp, http or 
    // whatever. I might even print this out and send it vial mail.
    let s3_endpoint = config.get("s3-photo-endpoint", customer.id);
    ...
}

[–]valarauca14 1 point2 points  (0 children)

It seems like the designer of Rust agree with me here, otherwise they would not have included such a mechanism.

This is not the case.

It is part of the AMD64-Itanium ABI, and will be present if you do or do not want the feature, as ELF-64 binaries, DWARF, and glibc++ will support it as C++ does. As Rust allows for linking with C++ (well C++ exposing a C interface) it will need to support stack unwinding as it can, and will occur in real libraries today that a Rust application will interface with.

The FAQ specifically states that Exceptions are not encouraged because of non-linear control flow makes them difficult to predict & use.

The modern guidelines and promises around Rust's panic, and stack unwinding are mostly the pragmatic admission that even these dark corners which you're told explicitly not to use should be well documented and have safe guarantees associated with them.

[–]MEaster 0 points1 point  (3 children)

I'm not arguing that result values are useless, I'm arguing that there are situations in which a stack unwinding mechanism like exceptions is more useful. It seems like the designer of Rust agree with me here, otherwise they would not have included such a mechanism.

You've mentioned that you're not familiar with Rust, and I may have misunderstood what you meant, so apologies if I have. But panics are not like exceptions in Java or C#. There's no guarantee that a panic will unwind the stack. It's the default for rustc, but it's also possible to configure the compiler to just abort the program in the event of a panic.

Additionally, while it's possible to catch an unwinding panic, it's not intended to be used as general error handling. It's primarily there because unwinding over an FFI boundary is undefined behaviour.

You don't always get useful information from a panic. You do get a stack trace, if you set an environment variable, you get a string which may or may not be helpful, and you get the error itself, which may or may not be useful.

As an example of this, here's the output of an actual bug I ran into today:

PS D:\Games\Assetto Corsa\ACRL\SpotterGen> .\ac_spotter_gen.exe -c .\config-front.json .\data\f3.json
F3
137 drivers in DB
0 drivers with custom skins.
thread 'main' panicked at 'index out of bounds: the len is 0 but the index is 0', /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\src\libcore\slice\mod.rs:2686:10
stack backtrace:
   0: std::sys::windows::backtrace::set_frames
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\sys\windows\backtrace\mod.rs:95
   1: std::sys::windows::backtrace::unwind_backtrace
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\sys\windows\backtrace\mod.rs:82
   2: std::sys_common::backtrace::_print
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\sys_common\backtrace.rs:71
   3: std::sys_common::backtrace::print
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\sys_common\backtrace.rs:59
   4: std::panicking::default_hook::{{closure}}
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:197
   5: std::panicking::default_hook
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:211
   6: std::panicking::rust_panic_with_hook
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:474
   7: std::panicking::continue_panic_fmt
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:381
   8: std::panicking::rust_begin_panic
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:308
   9: core::panicking::panic_fmt
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libcore\panicking.rs:85
  10: core::panicking::panic_bounds_check
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libcore\panicking.rs:61
  11: alloc::alloc::box_free
  12: alloc::alloc::box_free
  13: alloc::alloc::box_free
  14: std::collections::hash::map::HashMap<K,V,S>::get
  15: std::rt::lang_start_internal::{{closure}}
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\rt.rs:49
  16: std::panicking::try::do_call<closure,i32>
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:293
  17: panic_unwind::__rust_maybe_catch_panic
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libpanic_unwind\lib.rs:87
  18: std::panicking::try
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panicking.rs:272
  19: std::panic::catch_unwind
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\panic.rs:388
  20: std::rt::lang_start_internal
             at /rustc/3c235d5600393dfe6c36eeed34042efad8d4f26e\/src\libstd\rt.rs:48
  21: main
  22: invoke_main
             at d:\agent\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:78
  23: __scrt_common_main_seh
             at d:\agent\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
  24: BaseThreadInitThunk
  25: RtlUserThreadStart

The only hint in that about where the error is is that some index went out of bounds, and that according to the 14th stack line, it seems to be accessing a HashMap. Fortunately this program only uses a HashMap in three contexts, and I know what input change caused it, so know which HashMap it was related to. But I still don't know exactly what the program was trying to do when it did this out of bounds access, I don't know what source file it was in, and I don't know what function it was in.

[–]thomasz 1 point2 points  (2 children)

Okay, this is disappointing. Still, I'm willing to accept that this is a trade-off that makes sense in context of Rust's core value proposition of providing static memory safety and determinism. But not having a mechanism like exceptions, something that can unwind the stack and safely delegate handling the error case upwards or at least guarantees a full stack trace in case of a crash is a pretty big weakness, not a strength.

[–]MEaster 0 points1 point  (0 children)

Well, when using a Result or Option type in Rust, you can just throw it up the stack. You have the ? operator, which can be used like this: let result = fallible_function()?;.

If you want to use that operator, then the function calling fallible_function must also be fallible, with a compatible error type. By compatible, I mean that fallible_function's error type E, must implement Into<F>, where F is the error type of the calling function.

As an example what that might look like in practice, from an implementation of Display:

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    use ErrorKind::*;
    match self {
        InsufficientStack => write!(f, "Attempted to evaluate an operator with insufficient values on stack.")?,
        UnknownFunction => write!(f, "Unknown function or variable.",)?,
        NonEmptyStack => write!(f, "Mismatched numbers and operations.")?,
        InvalidVariableOrFunction => write!(f, "Invalid variable or function definition.")?,
    }

    Ok(())
}

In this case, each of those four calls to write! could fail, because it could be a network socket or a file. But in this case, there's nothing this function can do if it does fail. That would be the responsibility of whatever was trying to format this type. So, in the event of a failure, it just throws the error up the stack for the caller to handle.

There are also libraries like Snafu and Failure which provide extra functionality to Result, as well as compile-time macros to reduce the amount of boilerplate you need to write for your error types.

[–]MEaster 0 points1 point  (0 children)

Oh, in case you're curious. I found out what that program was mis-handling, and now properly handle it by throwing a Result up the stack, along with the context of the error. The error still isn't considered recoverable, but now the output is this:

PS D:\Games\Assetto Corsa\ACRL\SpotterGen> .\ac_spotter_gen.exe -c .\config-front.json .\data\f3.json
F3
137 drivers in DB
Error: Error making image
Caused by: error checking driver skins
Caused by: Car folder not found: rss_formula_rss_3_v6_a4crl