[outcome] narrow-contract, wide-contract, and value_if
Hi Everyone, Some comments about `outcome`'s observers with narrow and wide contracts. First, the wide-contract functions. We have a situation when someone assumes that `o` currently stores an error_code, and with this assumption in mind calls `o.error()`, but somehow his assumption is incorrect. What do we want to do? 1. Try to report programmer's bug at run-time? 2. Since in this case it is a bug anyway and are screwed, at least let's make such behavior useful for other situations where `error()` is called conciously on `o` without an error code? The first scenario attempts at identifying a bug, bringing it to attention of developers (maybe in form of a log), and minimizing its consequences by perhaps skipping the execution of some part of the program in hope that the bug will no show symptoms again. The second scenario in a way ignores the bug, and lets the program continue, as though it was correct, and from now on the program is doing something else than what the programmer intended. But the semantics of the function are useful in itself. Users who check `o.has_error()` before calling it can use it as if `error()` had a single purpose and a narrow contract; and users who know the additional semantics can use it for other useful things, like saving themselves of writing some boilerplate code. It is my impression that some of these observer functions in `outcome` follow this second scenario: just do something useful for people who use it unchecked. Saving boilerplate is useful, but let's state it clear: these functions offer no protection against inadvertent mistakes: no run-time or static protection. If I call `o.error()` and it throws when `o.is_empty()`, it is a form of protection. If `o.error()` returns `E{}` when `o.has_value()`, this is no protection, it is convinience function that can be used as a narrow-contract observer. Maybe the interface of `outcome` should make this distinction and offer two sets: one for observers with run-time protection, and the other for nice convenience functions? Second, narrow-contract functions. Narrow functions acheive the following goals: 1. Simple design: function only does one thing, it does not have any if inside. 2. The interface of the library makes a clear distinction of what is valid and what is an invalid usage of the library. It helps in code reviews. For instance, you are looking for a bug in a big code, you find the following piece: ``` auto e = o.error(); ``` and it turns out that `e` contains a value. Did you just find a bug, or was it the author's intention to get an `E{}` at this point? You do not know with wide contracts. With narrow contracts you know: it is a bug. 3. Narrow contracts assist instrumentation in test builds and runs. If I have function `just_value` with a narrow contract, I leave myself the freedom: I can do whatever I want when has_value() is false. What can I do with this freedom: A. In test build, I can check the precondition at run-time and launch the debugger B. I can (run-time) check the precondition, and std::termiante() or throw an exception. C. I can ignore the potential precondition violation, check nothing and aim at some performance gain. D. I can put some code that assists the static analyser (like __builtin_unreachable()) E. I can put some instrumentation code that assists an UB sanitizer (like __builtin_unreachable()) F. I can put hints for optimizer (like __builtin_unreachable(), again) G. Finally, I can put a macro `PRECONDITION(has_value())` which can do any of the above based on the build configuration. Given that, the proposed function `value_if` (which returns a pointer to value) will not satisfy my needs. It has a wide contract, so I cannot put macro `PRECONDITION()` inside. It rturns a pointer. The pointer's operator-> inded does have a narrow contract, but it gives me no room where I can put macro `PRECONDITION()`. So I get some narrow contract, but without the benefits I am looking for. So that is my view of the observer functions. Now my proposal is to provide a pair of observers for each value, error_code and exception -- narrow and wide: `value()` and `just_value()`, `error()` and `just_error()`, `exception()` and `just_exception()` Or maybe, even three for narrow-contract, wide-contract with run-time protection, and for convenience function: `error()` -- wide-contract, runtime-checked precondition `just_error()` -- narrow contract `as_error()` -- convenience function, e.g. returns E{}, on value, monad::has_exception on exception, etc.. Regards, &rzej;
On 31/05/2017 10:36, Andrzej Krzemienski wrote:
First, the wide-contract functions.
We have a situation when someone assumes that `o` currently stores an error_code, and with this assumption in mind calls `o.error()`, but somehow his assumption is incorrect. What do we want to do?
1. Try to report programmer's bug at run-time? 2. Since in this case it is a bug anyway and are screwed, at least let's make such behavior useful for other situations where `error()` is called conciously on `o` without an error code?
Perhaps this is where our philosophical difference lies.
I don't consider calling error() a bug *at any time*, even if the object
does not explicitly contain an error_code.
An outcome is *not* a variant and does not have the same semantics as one.
(Having said that, this argument applies only to result/outcome, where
the error type is known and can be sanely constructed to indicate "no
error occurred". expected
The second scenario in a way ignores the bug, and lets the program continue, as though it was correct, and from now on the program is doing something else than what the programmer intended. But the semantics of the function are useful in itself. Users who check `o.has_error()` before calling it can use it as if `error()` had a single purpose and a narrow contract; and users who know the additional semantics can use it for other useful things, like saving themselves of writing some boilerplate code.
What if the programmer intends to get an error_code that indicates success, perhaps for logging purposes? Like I said, this should not be considered a bug in the first place, and the program *is* correct.
2017-05-31 1:30 GMT+02:00 Gavin Lambert via Boost
On 31/05/2017 10:36, Andrzej Krzemienski wrote:
First, the wide-contract functions.
We have a situation when someone assumes that `o` currently stores an error_code, and with this assumption in mind calls `o.error()`, but somehow his assumption is incorrect. What do we want to do?
1. Try to report programmer's bug at run-time? 2. Since in this case it is a bug anyway and are screwed, at least let's make such behavior useful for other situations where `error()` is called conciously on `o` without an error code?
Perhaps this is where our philosophical difference lies.
I don't consider calling error() a bug *at any time*, even if the object does not explicitly contain an error_code.
An outcome is *not* a variant and does not have the same semantics as one.
(Having said that, this argument applies only to result/outcome, where the error type is known and can be sanely constructed to indicate "no error occurred". expected
can't make that assumption so it's much more like a plain variant. This is actually one of the things I dislike about expected.) The second scenario in a way ignores the bug, and lets the program
continue, as though it was correct, and from now on the program is doing something else than what the programmer intended. But the semantics of the function are useful in itself. Users who check `o.has_error()` before calling it can use it as if `error()` had a single purpose and a narrow contract; and users who know the additional semantics can use it for other useful things, like saving themselves of writing some boilerplate code.
What if the programmer intends to get an error_code that indicates success, perhaps for logging purposes? Like I said, this should not be considered a bug in the first place, and the program *is* correct.
I guess I was not able to convey my reasoning as clearly as I would like to. I know what you are saying. You want to have a function in the interface that returns a non-zero error_code when, when `o.has_error()` and a zero-error when `o.has_value()`. There is a demand for it, `outcome` should provide for it, this way or the other. On the other hand, because the current names go in pairs (has_valeue() and value(), has_error() and error()) that someone can get the the mental model of a `variant`. Because of all the confusion reported in this review, my suggestion would be to indicate clearly which of the observers is just a `getter` for one of the variant states (possibly with run-time checks), and which is a "convenience" function (which may observe more than one variant state). And once you indicate it in the docs, maybe also indicate it is the function name. This way `error()` would mean "observe the state of the variant (or sum) element corresponding to `has_error()`, ans `as_error()` would say "interpret the entire `outcome` as an error_code". f course I am still not sure how error_code and exception_ptr mix in these functions, so I may be confusing some people. Regards, &rzej;
participants (2)
-
Andrzej Krzemienski
-
Gavin Lambert