How can I write a std::apply on a std::expected?
Asked Answered
G

7

17

In C++23, given:

expected<A, string> getA(const X& x);
expected<B, string> getB(const Y& y);

C compute_all(const A& a, const B& b);

Is there a way to avoid a classic style check like:

auto a_ret = getA(x);
if (!a_ret)
  return a_ret.error();

auto b_ret = getB(y);
if (!b_ret)
  return b_ret.error();

C final_ret = compute_all(*a_ret, *b_ret);

and write something like

expected<C, string> final_ret = magic_apply(compute_all, getA(x), getB(y))

This is an idea of implementation of magic_apply but I need something more generic (perhaps using variadic templates), that allows passing to compute_all some parameter that is not an std::expected.

template<typename Func, typename A, typename B, typename Err>
auto magic_apply(Func func, const std::expected<A, Err>& a, const std::expected<B, Err>& b)
->
std::expected<decltype(func(a.value(), b.value())), Err>
{
    if(!a) {
        return std::unexpected{ a.error() };
    }
    if(!b) {
        return std::unexpected{ b.error() };
    }
    return func(a.value(), b.value());
}

Does some feature already exist in the language I could use to write this?

Geophysics answered 11/7, 2024 at 9:47 Comment(25)
And what if getA or getB does not return an expected? Or what if computer_all returns an expected?Geophysics
In Haskell this is called liftA2 and in Rust the ! syntax expands to the pattern you wrote. C++ has neither of these things, I'm afraid.Rehash
What's wrong with a for loop? Too easy, too readable?Somnus
@Somnus No, just impossible. These are function arguments; how would you write a for loop that iterates over them?Audun
@cigien: you might iterate over a std::array<std::optional<Err>, N>, and if no error just call return func(expecteds.value()...)Fernferna
@Audun Object-orientated design comes to mind. Make a class "compute thingie" then inherit that one in whatever different scenarios you have. A compute function per object is called from a loop. Perfectly readable code and no need to invoke the nine hells of template meta programming.Somnus
@Somnus how does writing that class help you generalise calling functions with arbitrary numbers of arguments?Sosa
Is it particularly good to return the first error, rather than a composite of every error present in the arguments?Sosa
@Sosa Simply don't create function APIs with arbitrary numbers of arguments - it's the wrong solution without a problem that it solves. Do you have an application specification saying that you should create a program which behaves in arbitrary ways? If not, then where is that requirement coming from?Somnus
@Fernferna That's basically the solution I was trying to implement :)Audun
@Somnus no, you have an application with a variety of functions, and the inputs to those functions can fail. At the top level, you want return frobnicate(get_foo(), get_bar(), get_baz());, but those can all failSosa
@Sosa With proper program design you can design those functions to use the same API then call them from a loop. If proper program design is absent, then you can use the OO solution of wrapper classes, optionally with inheritance, to spank the bad program design into the unison API it should have been written with to begin with.Somnus
@Lundin: I won't say that having functions with more than one parameter is a bad design...Fernferna
@Somnus there is no loop. get_foo returns the first parameter of frobnicate on the happy path, and an error otherwise. ditto get_bar and get_baz. Those parameters aren't the same type, and there is a fixed number of themSosa
@Fernferna Nobody said that either? But having functions with different parameter types supposed to be used for the same purpose by the same part of the program is bad design. Functions shouldn't just end up in a way because a cat walked over the keyboard - there should be a plan behind it all.Somnus
And yes this is the very definition of an XY question. Except people here tends to take such questions to the next level, proposing various Z to solve the solution Y, while the OP really ought to be solving the problem X...Somnus
@Somnus What does it matter what the OP "really ought to be doing"? This isn't a help-desk, it's a resource for future programmers who might have the same problem. Of course, it sounds like you think that any programmer who has a need for this is doing something fundamentally wrong ...Audun
@Somnus the question here is how to call R frobnicate(A, B, C) when you have expected<A, Err>, expected<B, Err>, expected<C, Err>, and end up with an expected<R, Err> (or possibly expected<R, composite<Err>>)Sosa
@Somnus "But having functions with different parameter types supposed to be used for the same purpose by the same part of the program is bad design". Are you saying function overloads are a bad design, or am I misunderstanding that statement?Audun
@Somnus it isn't using different functions for the same purpose. It's marshalling specific arguments for a particular function into a suitable form.Sosa
@Somnus it's the same as asking how to write one cartesian_product for functions of multiple arities (and the appropriate number of input vectors), instead of cartesian_product_binary, cartesian_product_ternary etc.Sosa
Can't you use the Head-Tail template pattern as in this (full-of-bugs) draft? godbolt.org/z/81z6Kjq1ePia
@fiorentinoing: fixed version.Fernferna
@Fernferna looks good, I'll try it sooner or later with and_then to check if is a possible use case.Pia
Related. Btw, curious to know what solution @Somnus proposes.Sightread
B
12

A generic version can be implemented with the help of std::optional and fold-expression:

#include <expected>
#include <functional>
#include <optional>

template<typename Func, typename... Args, typename Err>
std::expected<std::invoke_result_t<Func&, const Args&...>, Err>
magic_apply(Func func, const std::expected<Args, Err>&... args)
{
  if (std::optional<Err> err;
      ([&] {
        if (!args.has_value())
          err.emplace(args.error());
        return err.has_value();
       }() || ...)
     )
    return std::unexpected{*err};

  return std::invoke(func, args.value()...);
}

Demo

Bronchiole answered 11/7, 2024 at 12:22 Comment(2)
I'm not sure of the order of expansion, but will this return the first error, or the last one?Audun
@Audun the first, || still short circuits in a foldSosa
A
7

You could implement magic_apply like this:

template<typename F, typename ...Ts> 
auto magic_apply(F && f, Ts... ts) 
{
    // construct an array<pair<bool, string>>, where the bool indicates an error, and the string the error message
    auto errs = std::to_array({ std::make_pair(ts.has_value(), ts.has_value() ? "" : ts.error()) ...});
    
    // Find an element that actually contains an error.
    auto itr = std::ranges::find_if(errs, [](const auto & pair) {
        return pair.first == false;
    });
    
    auto ret = std::expected<decltype(f(*ts...)), decltype(itr->second)>{};

    // Either we return the error
    if (itr != errs.end()) 
        ret = std::unexpected(itr->second);
    // or the result of calling the function on all the expecteds
    else ret = f(*ts...);

    return ret;
}

Here's a demo.

Audun answered 11/7, 2024 at 11:50 Comment(0)
S
4

If you curry your function, you can apply it like this:

struct transformer_t {
    template <typename F, typename T, typename... Ts>
    auto operator()(F&& curried, T&& next, Ts&&... rest){
        return curried.and_then([&]<typename G>(G&& g){ 
            return (*this)(std::forward<T>(next).transform(std::forward<G>(g)), std::forward<Ts>(rest)...); 
        });
    }
    template <typename R>
    auto operator()(R&& result){
        return std::forward<R>(result);
    }
};

template <typename... Ts>
using common_error_t = std::common_type_t<typename Ts::error_type...>;

template <typename F, typename... Ts>
requires std::invocable<F, typename Ts::value_type...>
std::expected<std::invoke_result_t<F, typename Ts::value_type...>, common_error_t<Ts...>> transform(F&& f, Ts&&... ts){
    using curry_t = decltype(curry(std::forward<F>(f)));
    std::expected<curry_t, common_error_t<Ts...>> curried = curry(std::forward<F>(f));
    return transformer_t{}(curried, std::forward<Ts>(ts)...);
}
Sosa answered 11/7, 2024 at 10:58 Comment(0)
F
4

Since the error types are same, a pair of and_then with transform will work fine:

return a.and_then([&](auto&& a/*hide the captured one*/){
      return b.transform([&](auto&& b/*hide the captured one*/){
            return func(a,b);
      });// b.transform
});// a.and_then

It works like and operator; if a is invalid, then a.error is returned. The inner transform only replaces value with return of func, and_then needs an operand returning expected with same error type. If there are more than 2 operands, you can chain and_thens till the last one that transforms. The general case - where error types are different - is difficult to handle; the compound expected type needs careful thought:

  • expected<result,variant<error1,error2>>
  • expected<result,common_type_t<error1,error2>>
  • expected<expected<result,error2>,error1>
  • expected<result,error1>
  • expected<result,error2>
  • else?

Each option has its own advantages and downsides. You can check for more monadic functions in the documentation for other cases. if (a.has_value()) should be the last resort, and dereference operator (*a) almost never should be used.

Fernandez answered 11/7, 2024 at 12:41 Comment(4)
I don't like the nested (scoped) lambdas. Can with the current standard we achieve something better?Pia
As I mentioned the main problem lies within combining the error types. If the error type is fixed, it is possible to define a variadic and_then; but since it lacks generic enough error handling , it is not in the std. If monadic interface is discarded, the case of unique error type may have a simpler solution.Fernandez
It is astonishing how much of this is so natural in Haskell. If a and b were Eithers and func was the binary function taking the two Right types, you'd write just func <$> a <*> b. And the requested f' could written in point-free style as f' = (<*>) . (f <$>).Sightread
I would also add that the proposed solution also matches the one sketched by the OP as regards the error case, in that it short-circuit, i.e. it returns a.error() even if both a and b are unexpected; Haskell does the same, with reference to the syntax I used in my previous comment.Sightread
S
4

While the other answers provide you with a way of getting an std::expected<C, std::string> from compute_all, I think that it is problematic to discard errors other than the first. We can write a type composite_error to hold those:

template<typename E>
struct composite_error
{
    std::vector<E> errors;
    void add(E error) { errors.push_back(std::move(error)); }
    void add(composite_error error) { errors.insert(errors.end(), std::move_iterator(error.errors.begin()), std::move_iterator(error.errors.end())); }
};

Then we can write our magic_apply in terms of composite_error, extracting a common error type.

template<typename F, typename... Ts>
requires expected_invocable<F, Ts...>
std::expected<std::invoke_result_t<F, expected_value_t<Ts>...>, composite_error<common_error_t<Ts...>>> magic_apply(F&& f, Ts&&... args)
{
    if (composite_error<common_error_t<Ts...>> error;
        ((args ? 0 : (error.add(std::forward<Ts>(args).error()), 1)) + ...) > 0){
        return std::unexpected{ std::move(error) };
    }
    return std::forward<F>(f)(std::forward<Ts>(args).value()...);
}

Demo

Sosa answered 11/7, 2024 at 16:19 Comment(0)
S
1

It's been too long since I've studied coroutines, and I've never actually used them in production code, so I can't really provide a working example¹, but coroutines are a way to deal with this.

If I remember correctly, the STL implementations could make std::expected an awaitable (and std::optional as well), which would allow writing code like this:

expected<A, string> getA(const X& x);
expected<B, string> getB(const Y& y);
C compute_all(const A& a, const B& b);

expected<C, string> coro_compute_all(expected<A, string> a, expected<B, string> b) {
    co_return compute_all(co_await a, co_await b);
}

where the coro_compute_all is essentially "lifting" compute_all into the expected monad.

Here's a presentation from Toby Allsopp about coroutines, and here is the part that is precisely about this topic, i.e. using coroutines to implement the optional monad (and hence the expected monad, which is not much more complicated).

There's also a repo with the code.

The above presentation is also referred to in the proposal P2561 by Barry Revzin, which is dated 2023-05-18, barely over a year ago, so you understand that the topic of what the best solution for your usecase is, is actually still a hot topic today.

A bit off topic, maybe, but this rabbit hole brought me to this talk as well, which is absolutily mind blowing.


(¹) Other than a very clunky and surely affected by UB example with an ugly imitation of std::optional here. I'm not really a coroutine expert at all, but I wanted to give a rough idea of the amount of code one would need to write.

Sightread answered 13/7, 2024 at 12:2 Comment(0)
R
0

Since std::expected<T,E>::value throws an exception, you could do:

template<typename Func, typename A, typename B, typename Err>
auto magic_apply(Func func, const std::expected<A, Err>& a, const std::expected<B, Err>& b)
->
std::expected<decltype(func(a.value(), b.value())), Err> {
    try {
        return func(a.value(), b.value());
    } catch (std::bad_expected_access<Err>& e) {
        return std::unexpected(std::move(e.error()));
    }
}
Rehash answered 11/7, 2024 at 10:4 Comment(1)
I think one of the reasons for std::expected was that errors can happen frequently and should not be considered an exception, so hiding throw in this implementation goes against that I think. I have to admit I do not have a better or any solution.Rabb

© 2022 - 2025 — McMap. All rights reserved.