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?