This question is very similar to "How can we forward a failed std::expected up through the call stack". Given a chain of function calls that each return std::expected values, errors should percolate back up the chain.
The wrinkle here is that, although the error type is the same at all level, the expected types vary. As far as I can tell, that means you can't use the monadic operations to streamline the code.
Consider this toy recursive descent parser:
#include <cstdlib>
#include <expected>
#include <iostream>
#include <string>
#include <utility>
struct Error { std::string msg; };
struct Bar { int x; };
struct Foo { Bar pair[2]; };
std::expected<Bar, Error> ParseBar() {
return std::unexpected<Error>("syntax error in Bar");
}
std::expected<Foo, Error> ParseFoo() {
auto bar0 = ParseBar();
if (!bar0.has_value()) return std::unexpected{std::move(bar0.error())};
auto bar1 = ParseBar();
if (!bar1.has_value()) return std::unexpected{std::move(bar1.error())};
Foo foo;
foo.pair[0] = std::move(bar0.value());
foo.pair[1] = std::move(bar1.value());
return foo;
}
int main() {
auto foo = ParseFoo();
if (!foo.has_value()) {
std::cerr << foo.error().msg;
std::exit(EXIT_FAILURE);
}
return 0;
}
I find this part of the syntax a bit unwieldy:
return std::unexpected{std::move(bar0.error())}
The boilerplate code visually buries the bar0.error(), which is the key to understanding the intent. (A helper function can help, but I don't consider it a great solution.)
Am I missing something that can be done with monadic operations?
The wrinkle here is that, although the error type is the same at all level, the expected types vary. As far as I can tell, that means you can't use the monadic operations to streamline the code.
Far from it. The monadic operations and_then and transform are designed to allow the expected type to vary. Similarly, or_else and transform_error allow the error type to vary. There is only a problem if you want to change both types at the same time.
The only wrinkle I came across was wrapping my head around the difference between and_then and transform, but cppreference.com cleared that up for me. (The functional passed to and_then returns a std::expected while that passed to transform returns a value to be wrapped in a std::expected.) Once I knew which operation to use, the rest fell into place for me.
std::expected<Foo, Error> ParseFoo() {
return ParseBar().and_then([](auto&& bar0){
return ParseBar().transform([&bar0](auto&& bar1){
Foo foo;
foo.pair[0] = std::move(bar0);
foo.pair[1] = std::move(bar1);
return foo;
});
});
}
One downside of the monadic operations is that they are monadic, not polyadic or variadic (i.e. they operate on one thing, not multiple). If the array were to get much larger, the indentation level would get out of hand. And if the size was arbitrary, say a vector instead of an array, this straight-forward approach would fail. Perhaps some sort of recursion could be used, but that might be more complexity than is warranted. Maybe it would be better to consider options other than monadic operators.
(A helper function can help, but I don't consider it a great solution.)
I disagree about the "great solution" part. The right helper function template could greatly help your code. The one I have in mind is a conversion operator from your Error type to a std::expected containing the unexpected error, templated over expected types. Since you already have a dedicated "error" type, this seems like a logical conversion to have. I would even consider allowing it to be an implicit conversion.
struct Error {
std::string msg;
template <class T, class Self>
operator std::expected<T, Error>(this Self&& self) {
return std::unexpected{std::forward<Self>(self)};
}
// Or, if your compiler is not new enough:
//template <class T>
//operator std::expected<T, Error>() const & {
// return std::unexpected{*this};
//}
//template <class T>
//operator std::expected<T, Error>() && {
// return std::unexpected{std::move(*this)};
//}
};
With this operator,
return std::unexpected{std::move(bar0.error())};
becomes
return std::move(bar0.error());
which removes one layer of burying. I do not know how to make this simpler other than giving up std::move or writing your own version of std::expected with fewer restrictions on construction. (A std::expected<T,E> can be constructed from a std::expected<U,G> but only if T can be constructed from U. This is the restriction to consider relaxing; be wary of other problems, though.)
If this syntax is simple enough for your tastes, it is fairly easy to change ParseFoo to use a loop and thus be adaptable to an arbitrary number of ParseBar calls.
std::expected<Foo, Error> ParseFoo() {
Foo foo; // or whatever will hold the `Bar` objects during parsing
unsigned count = -1;
while ( /* some condition, for example:*/ ++count < 2 ) {
if ( auto bar = ParseBar() )
foo.pair[count] = std::move(bar).value();
else
return std::move(bar).error();
}
return foo;
}