Composable monadic lambda/std::function with std::optional

Viewed 159

I'm attempting to build a Filter type that can act as a composable error-handling callable (i.e. a Maybe monad with << implementing >>=) that (ideally) works with (at least) lambda's and std::function in C++17 (no need for compatibility with older standards). Ideally, something that would work like this:

// two example Filter's instantiated from lambda's
Filter g = [](const int a, const int b) -> float {
                  return a + b; 
           };
Filter f = [](const float c) -> std::optional<int> {
              if c < 0 return std::nullopt;
              else return c + 1;
           };

// compose them - only two right now for brevity
auto h = f << g; // so that this implements f(g(...))

// and then later call them with some arguments
h(1.0, 2.0);

If any of the intermediate functions return std::nullopt, I need the entire composition to return std::nullopt and for this to work over any number of composed functions (that may or may not return optionals).

The implementation that I have so far (inspired by this post) works for std::function when composing two simple functions but regularly fails when combining multiple functions that change whether there return types are optional or not since the type deduction is extremely fragile.

#include <functional>
#include <optional>
#include <type_traits>

template <typename TReturn, typename... TArgs>
class Filter {

  public:

  /**
   * The filter function that we evaluate.
   */
  std::function<TReturn(TArgs...)> eval_;

  /**
   * Construct a filter from a std::function.
   */
  Filter(std::function<TReturn(TArgs...)> f) : eval_(f) {}

  /**
   * Apply the filter to given arguments.
   */
  auto operator()(TArgs... args) const {
    return this->eval_(args...);
  }

  /**
    * Compose this function, `f`, and `g`.
    */
  template <typename TOReturn, typename... TOArgs>
  auto operator<<(Filter<TOReturn, TOArgs...> other) const -> Filter<TReturn, std::optional<TOArgs>...> {

    // the result of the resulting function f(g(...))
    using TFReturn = std::optional<typename TReturn::value_type>;

    // the type of the resulting function f(g(...))
    using TFuncType = std::function<TFReturn(std::optional<TOArgs>...)>;

    // construct (and return) the composed function
    TFuncType f = [this, other](std::optional<TOArgs>... args) -> TFReturn {

                    // if we got a good value, perform the function composition
                    if ((args && ...)) {

                      // evaluate g over the input arguments
                      auto gresult = other(*(args)...);

                      // if we got a non-fail result from g, then call f
                      if (gresult) {

                        // and evaluate our own function over the result
                        return this->eval_(gresult);

                      }

                    } // END: if ((args && ...))

                    // if anything falls through, return failur
                    return std::nullopt;

                  };

    return f;

    } // END: operator<<


}; // END: class Filter


auto main() -> int {

  // a couple of test functions
  std::function<int(float, float)> f1 = [](const float a, const float b) -> int {
             return a * b;
           };

  std::function<std::optional<int>(int)> f2 = [](const int c) -> std::optional<int> {
               if (c < 0) return std::nullopt;
               else return c;
             };

  std::function<std::optional<int>(int)> f3 = [](const int d) -> std::optional<int> {
               if (d > 10) return 10;
               else return d;
             };

  // these construct fine if I have explicitly typed std::function's above.
  Filter f = f3;
  Filter g = f2;
  Filter h = f1;

  // build the composition
  auto F = g << h; // this works!
  // auto F = f << g << h; // this does not work

  // evaluate our composition over different arguments
  auto x = F(1.0, 2.0);
  auto y = F(-1.0, 2.0);

}


Composing two simple functions, f << g, works but f << g << h currently fails with the following error:

filter.cpp: In instantiation of ‘Filter<TReturn, std::optional<TOArgs>...> Filter<TReturn, TArgs>::operator<<(Filter<TOReturn, TOArgs ...>) const [with TOReturn = std::optional<int>; TOArgs = {int}; TReturn = std::optional<int>; TArgs = {int}]’:
filter.cpp:98:17:   required from here
filter.cpp:55:43: error: no match for call to ‘(const std::function<std::optional<int>(int)>) (std::optional<int>&)’   55 |                         return this->eval_(gresult);      |                                ~~~~~~~~~~~^~~~~~~~~
In file included from /usr/include/c++/10/functional:59,                 from filter.cpp:1:
/usr/include/c++/10/bits/std_function.h:617:5: note: candidate: ‘_Res std::function<_Res(_ArgTypes ...)>::operator()(_ArgTypes ...) const [with _Res = std::optional<int>; _ArgTypes = {int}]’  617 |     function<_Res(_ArgTypes...)>::      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
/usr/include/c++/10/bits/std_function.h:618:25: note:   no known conversion for argument 1 from ‘std::optional<int>’ to ‘int’  618 |     operator()(_ArgTypes... __args) const      |                ~~~~~~~~~^~~~~~~~~~
/usr/include/c++/10/bits/std_function.h:601:7: error: ‘std::function<_Res(_ArgTypes ...)>::function(_Functor) [with _Functor = Filter<TReturn, TArgs>::operator<< <std::optional<int>, {int}>::<lambda(std::optional<int>)>; <template-parameter-2-2> = void; <template-parameter-2-3> = void; _Res = std::optional<int>; _ArgTypes = {std::optional<int>}]’, declared using local type ‘Filter<TReturn, TArgs>::operator<< <std::optional<int>, {int}>::<lambda(std::optional<int>)>’, is used but never defined [-fpermissive]  601 |       function<_Res(_ArgTypes...)>::

How can I modify the type deduction to support std::optional<T>(...) and T(...) functions in arbitrary compositions? Or is there a fundamentally different way to approach this solution?

1 Answers

Not sure to understand your code but... seems to me that the problem is in this section (removed comments and simplified a little)

auto gresult = other(*(args)...);

if (gresult)
   return this->eval_(gresult);

What type is gresult?

I suppose you expect that is a std::optional of some type, so you test if is valid (if (gresult)) and pass it to the following eval_().

But what happens combining g() and h()? And combining also f()?

If I understand correctly, h() is executed before and return a int; so gresult is a int.

When you check

if (gresult)

you don't check if gresult is valid, but if gresult is zero or not.

But this works (I mean... compile) and the integer value is used to call g() that waits for a int const.

But when the result of g()

auto gresult = other(*(args)...);

(that is a std::optional<int>) is used to call f() (that waits for a int), you have that

if (gresult)

correctly check if gresult is valid, but when you call f()

   return this->eval_(gresult);

you pass to f() a std::optional<int>(), not a int.

Not sure about the solution (not sure about what you exactly want and about the std::option deduction guides) but seems to me that you should impose that gresult is a std::option of some type

std::option  gresult{ other( args.value()... ) };

so the following check

if ( gresult )

is ever about the validity of gresult, next you have to pass the value of gresult to the following function

return this->eval_( gresult.value() );

Observe that, in the following code,

int a;

std::optional  b{a};
std::optional  c{b};

static_assert( std::is_same_v<decltype(b), std::optional<int>> );
static_assert( std::is_same_v<decltype(c), std::optional<int>> );

both b and c are std::optional<int>

I mean... using deduction guides for std::optional, if the argument is a std::optional, the resulting type is the same.

I suppose that this should works, for you, but I'm not sure about what you exactly want.

Related