diff --git a/doc/expected.md b/doc/expected.md index 3e22e9e..35bdaa2 100644 --- a/doc/expected.md +++ b/doc/expected.md @@ -1,5 +1,173 @@ # expected +## Description + +The class `expected` presented here is an extended version of `expected` as +proposed in [P0323R1](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0323r1.pdf) +and the subsequent [D0323R2](https://github.com/viboes/std-make/blob/master/doc/proposal/expected/d0323r2.md). + +The main difference is that this class takes more than one error type, which makes it more +flexible. One example of a type of the `expected` family, [outcome](https://ned14.github.io/boost.outcome/), +can store either an error of type `std::error_code`, or an exception in the form of `std::exception_ptr`. +This can be represented naturally in this implementation via `expected`. + +In addition, libraries would generally differ in their choice of error types. It would be a +common need in practice of having to combine the results of calling two different libraries, +each with its own error type, such as the two examples below: + + // Library 1 + + namespace lib1 + { + + enum class error + { + division_by_zero, + other_error + }; + + expected div( double x, double y ); + + } // namespace lib1 + + // Library 2 + + namespace lib2 + { + + enum class error + { + division_by_zero, + negative_logarithm + }; + + expected log( double x ); + + } // namespace lib2 + +In this proposal, combining the results of `lib1::div` and `lib2::log` can be achieved via +simple composition: + + expected log_div_mul( double x, double y, double m ) + { + auto r1 = lib1::div( x, y ); + if( !r1 ) return r1.unexpected(); + + auto r2 = lib2::log( r1.value() ); + if( !r2 ) return r2.unexpected(); + + return m * r2.value(); + } + +An alternative approach that requires more effort is also supported: + + enum class common_error + { + division_by_zero, + negative_logarithm, + other_error, + unknown_error + }; + + common_error make_common_error( lib1::error e ); + common_error make_common_error( lib2::error e ); + + expected log_div_mul2( double x, double y, double m ) + { + static const auto rm = []( auto x ) { return make_common_error(x); }; + + auto r1 = lib1::div( x, y ).remap_errors( rm ); + if( !r1 ) return r1.unexpected(); + + auto r2 = lib2::log( r1.value() ).remap_errors( rm ); + if( !r2 ) return r2.unexpected(); + + return m * r2.value(); + } + +When an attempt to access the value via `r.value()` is made and an error is present, +an exception is thrown. By default, this exception is of type `bad_expected_access`, +as in D0323R2, but there are two differences. First, `bad_expected_access` objects +derive from a common base `bad_expected_access` so that they can be caught at +points where the set of possible `E` is unknown. + +Second, the thrown exception can be customized. The implementation calls +`throw_on_unexpected(e)` unqualified, where `e` is the error object, and the user can +define such a function in the namespace of the type of `e`. Two specialized overloads +of `throw_on_unexpected` are provided, one for `std::error_code`, which throws the +corresponding `std::system_error`, and one for `std::exception_ptr`, which rethrows +the exception stored in it. + +For example, `lib1` from above may customize the exceptions associated with `lib1::error` +via the following: + + namespace lib1 + { + + enum class error + { + division_by_zero, + other_error + }; + + class exception: public std::exception + { + private: + + error e_; + + public: + + explicit exception( error e ): e_( e ) {} + virtual const char * what() const noexcept; + }; + + void throw_on_unexpected( error e ) + { + throw exception( e ); + } + + } // namespace lib1 + +In this implementation, `unexpected_type` has been called `unexpected_` and is +an alias for `variant`. It is unfortunately not possible to use the name `unexpected`, +because a function `std::unexpected` already exists. + +The `make_...` helper functions have been omitted as unnecessary; class template argument deduction +as in `expected{ 1.0 }` or `unexpected_{ lib1::division_by_zero }` suffices. + +Other functions have also been dropped as unnecessary, not providing sufficient value, dangerous, or +a combination of the three, although the decision of what to include isn't final at this point. The aim +is to produce a minimal interface that still covers the use cases. + +`expected` can be converted to `expected` if all error types in `E1...` are +also in `E2...`. This allows composition as in the example above. Whether value convertibility ought +to also be supported is an open question. + +A single monadic operation ("bind") is supported in the form of `operator>>`, allowing + + auto log_div_mul3( double x, double y, double m ) + { + return lib1::div( x, y ) >> [&]( auto && r1 ) { + + return lib2::log( r1 ) >> [&]( auto && r2 ) -> expected { + + return m * r2; + + }; + }; + } + +as well as the more concise in this example, although limited in utility for real world scenarios, + + auto log_div_mul3( double x, double y, double m ) + { + return lib1::div( x, y ) >> std::bind>( lib2::log, _1 ) >> m * _1; + } + +The more traditional name `then` was also a candidate for this operation, but `operator>>` has two advantages; +it avoids the inevitable naming debates and does not require parentheses around the continuation lambda. + ## Synopsis // unexpected_