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

290 Views Asked by At

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.)

2

There are 2 best solutions below

0
On BEST ANSWER

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...

0
On

Maybe your problem is more subtle than what I can understand, but this quite simple solution seems to do the trick (as far as I can tell).

/**
  g++ -std=c++17 -o prog_cpp prog_cpp.cpp \
      -pedantic -Wall -Wextra -Wconversion -Wno-sign-conversion \
      -g -O0 -UNDEBUG -fsanitize=address,undefined
**/

#include <iostream>
#include <optional>

template<typename Predicate1,
         typename Predicate2>
auto
lazy_and(Predicate1 p1,
         Predicate2 p2)
{
  return [&](const auto &arg)
  {
    return p1(arg)&&p2(arg);
  };
}

int
main()
{
  // basic predicates
  const auto has_value=[&](const auto &opt){ return opt.has_value(); };
  const auto is_positive=[&](const auto &opt){ return opt.value()>0; };
  // combination of predicates
  const auto has_value_and_is_positive=lazy_and(has_value, is_positive);
  const auto is_positive_and_has_value=lazy_and(is_positive, has_value);
  // test
  const auto test_predicate=
    [&](const auto &title, const auto &predicate, const auto &arg)
    {
      try
      {
        const auto result=predicate(arg);
        std::cout << title << " --> " << result << '\n';
      }
      catch(const std::exception &e)
      {
        std::cerr << title << " !!! " << e.what() << '\n';
      }
    };
  std::cout << "~~~~ empty ~~~~\n";
  const auto empty=std::optional<int>{};
  test_predicate("has_value",
                 has_value, empty);
  test_predicate("is_positive",
                 is_positive, empty);
  test_predicate("has_value_and_is_positive",
                 has_value_and_is_positive, empty);
  test_predicate("is_positive_and_has_value",
                 is_positive_and_has_value, empty);
  std::cout << "~~~~ positive ~~~~\n";
  const auto positive=std::optional<int>{5};
  test_predicate("has_value",
                 has_value, positive);
  test_predicate("is_positive",
                 is_positive, positive);
  test_predicate("has_value_and_is_positive",
                 has_value_and_is_positive, positive);
  test_predicate("is_positive_and_has_value",
                 is_positive_and_has_value, positive);
  std::cout << "~~~~ negative ~~~~\n";
  const auto negative=std::optional<int>{-3};
  test_predicate("has_value",
                 has_value, negative);
  test_predicate("is_positive",
                 is_positive, negative);
  test_predicate("has_value_and_is_positive",
                 has_value_and_is_positive, negative);
  test_predicate("is_positive_and_has_value",
                 is_positive_and_has_value, negative);
}

/**
~~~~ empty ~~~~
has_value --> 0
is_positive !!! bad optional access
has_value_and_is_positive --> 0
is_positive_and_has_value !!! bad optional access
~~~~ positive ~~~~
has_value --> 1
is_positive --> 1
has_value_and_is_positive --> 1
is_positive_and_has_value --> 1
~~~~ negative ~~~~
has_value --> 1
is_positive --> 0
has_value_and_is_positive --> 0
is_positive_and_has_value --> 0
**/