c++functional-programminglazy-evaluationshort-circuitingboost-hana

Combining predicates in a functional way and allowing short-circuiting to work


Foreword

I asked a similar question: say I have a predicate auto p1 = [](int x){ return x > 2; } and a predicate auto p2 = [](int x){ return x < 6; }, how do I combine p1 and p2 to obtain p1and2 such that p1and2(x) == p1(x) && p2(x)? The answer was use boost::hana::demux (refer to the linked question for details).

The new problem and the question

Sometimes, however, the evaluation of one predicate should occur only if the other predicate evaluates to a given truthness value, e.g. true.

For instance one predicate might be

constexpr auto has_value = [](std::optional<int> opt){ return opt.has_value(); };

and the other predicate

constexpr auto has_positive = [](std::optional<int> opt){ return opt.value() == 3; };

It's easy to recognize the following

bool b1 = has_value(some_opt_int) && has_positive(some_opt_int); // true or false, but just fine either way
bool b2 = has_positive(some_opt_int) && has_value(some_opt_int); // runtime error if !some_opt_int.has_value()

Defining

constexpr auto all = boost::hana::demux([](auto const&... x) { return (x && ...); });

and using it like so

std::optional<int> empty{};
contexpr auto has_value_which_is_positive = all(has_value, has_positive);
bool result = has_value_which_is_positive(empty);

would lead to failure at run time, because the it's the function call to the variadic generic lambda wrapped in all which forces the evaluation of its arguments, not the fold expression (x && ...).

So my question is, how do I combine has_value and has_positive to get has_value_which_is_positive? More in general, how do I "and" together several predicates such that they're evaluated only as much as the short-circuit mechanism requires?

My attempt

I think that, in order to prevent the predicates from being evaluated, I can have wrap them in some function objects which, when applied to the argument (the std::optional), give back another object which wraps the predicate and the std::optional together, and has an operator bool conversion function which would be triggered only when the fold expression is evaluated.

This is my attempt, which is miserably undefined behavior because the assertion in main sometimes fails and sometimes doesn't:

#include <optional>

#include <boost/hana/functional/demux.hpp>
#include <boost/hana/functional/curry.hpp>

template<typename P, typename T>
struct LazilyAppliedPred {
    LazilyAppliedPred(P const& p, T const& t)
        : p(p)
        , t(t)
    {}
    P const& p;
    T const& t;
    operator bool() const {
        return p(t);
    }
};

constexpr auto lazily_applied_pred = [](auto const& p, auto const& t) {
    return LazilyAppliedPred(p,t);
};

auto constexpr lazily_applied_pred_curried = boost::hana::curry<2>(lazily_applied_pred);

constexpr auto all_true = [](auto const&... x) { return (x && ...); };

constexpr auto all = boost::hana::demux(all_true);

constexpr auto has_value = [](std::optional<int> o){
    return o.has_value();
};
constexpr auto has_positive = [](std::optional<int> o){
    assert(o.has_value());
    return o.value() > 0;
};

int main() {
    assert(all(lazily_applied_pred_curried(has_value),
               lazily_applied_pred_curried(has_positive))(std::optional<int>{2}));
}

(Follow up question.)


Solution

  • I've just realized that an ad-hoc lambda to do what I described is actually very terse:

    #include <assert.h>
    #include <optional>
    
    constexpr auto all = [](auto const& ... predicates){
        return [&predicates...](auto const& x){
            return (predicates(x) && ...);
        };
    };
    
    constexpr auto has_value = [](std::optional<int> o){
        return o.has_value();
    };
    constexpr auto has_positive = [](std::optional<int> o){
        assert(o.has_value());
        return o.value() > 0;
    };
    
    int main() {
        assert(all(has_value, has_positive)(std::optional<int>{2}));
        assert(!all(has_value, has_positive)(std::optional<int>{}));
    }
    

    However, there's still something about short-circuiting in fold expressions that doesn't convince me...