Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ C++ does not have Refinement Types, so this library is a clumsy attempt to bring

## Motivation

Let's say that throughout our application we have functions that require **even** integers.
Let's say that throughout our application we have functions that require integers that are **even** and **less than 10**.

We could model this as an `int` and whenever we need it to be even we would validate it with "is_even" and make a decision based on the result. However, we risk duplicating the same validation step in different functions or, worse, we may forget to validate by mistakenly assuming it has already been validated.

Expand All @@ -21,33 +21,39 @@ Additionally, this has a nice second-order effect of pushing validation to the e
## Example

```cpp
// a refinement for ints constrained to be even.
using even =
rvarago::refined::refinement<int, [](auto const x) { return x % 2 == 0; }>;


// refinements can be further refined, e.g. all x such that x is even and x < 10.
using even_lt_10 =
rvarago::refined::refinement<int, [](auto const x) { return x < 10; }, even>;

auto do_something_else(even v) -> void {
// deep down in the call stack do we access its ground type.
int const x = v.value;
// act on the ground type as we see fit.
}

auto do_something(even v) -> void {
auto do_something(even_lt_10 v) -> void {
do_something_else(v);
}

int main() {
int const x = read_int();

if (std::optional<even> e = even::make(x); e) {
// the default error policy gives an std::optional back.
if (std::optional<even_lt_10> e = even_lt_10::make(x); e) {
do_something(*e);
}

return 0;
}
```

With this example, we notice that not all functions need access to the underlying `int` element and operate entirely on `even`. So we validate and convert the `int` element into `even` at the very beginning and only fall back to `int` at the very last moment, when we actually need it. Both operations should ideally at the edges of our applications.
With this example, we notice that not all functions need access to the underlying `int` element and operate entirely on `even_lt_10` or its "super-type" `even`. So we validate and convert the `int` element into `even_lt_10` at the very beginning and only fall back to `int` at the very last moment, when we actually need it. Both operations should ideally at the edges of our applications.

Although we reported errors via `std::optional` in the example, it's possible to customise it, e.g. to throw an exception.
Although we reported errors via `std::optional` in the example, we can customise it, e.g. to throw an exception with the built-in `even::make<refined::error::to_exception>` or define a whole user-provided policy.

## Requirements

Expand Down
47 changes: 39 additions & 8 deletions include/rvarago/refined.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,29 +68,60 @@ struct to_exception {

} // namespace error

// `refinement<T, Pred>` constraints values `t: T` where `Pred(t)` holds.
template <typename T, std::predicate<T const &> auto Pred> struct refinement {
// Reusable predicates.
namespace preds {

// Assumes all values of `T` are valid.
template <typename T> constexpr auto unconstrained(T const &) -> bool {
return true;
}
} // namespace preds

template <typename T, std::predicate<T const &> auto Pred, typename... Bases>
requires(std::is_same_v<T, typename Bases::value_type> && ...)
class refinement {
public:
using value_type = T;
using predicate_type = decltype(Pred);

// `check(value)` holds when `value` satisfies `Pred`.
static constexpr auto check(T const &value) -> bool {
return std::invoke(Pred, value);
}

// Ground value.
T value;

template <typename Base>
requires(std::is_same_v<Base, Bases> || ...)
constexpr /* implicit */ operator Base() const {
return Base::unverified_make(value);
}

// `make(value)` is the only factory to refinements.
//
// If `Pred(value)` holds, then this produces a valid instance of `T` by
// delegating to `policy.ok`. Else reports the failure via `policy.err`.
template <error::policy<refinement<T, Pred>> Policy = error::to_optional>
template <
error::policy<refinement<T, Pred, Bases...>> Policy = error::to_optional>
static constexpr auto make(T value, Policy policy = {})
-> Policy::template wrapper_type<refinement<T, Pred>> {
if (std::invoke(std::move(Pred), value)) {
return policy.template ok<refinement<T, Pred>>(
{refinement<T, Pred>{std::move(value)}});
-> Policy::template wrapper_type<refinement<T, Pred, Bases...>> {
if (check(value) && (Bases::check(value) && ...)) {
return policy.template ok<refinement<T, Pred, Bases...>>(
{refinement<T, Pred, Bases...>{std::move(value)}});
} else {
return policy.template err<refinement<T, Pred>>();
return policy.template err<refinement<T, Pred, Bases...>>();
}
}

// `unverified_make(value)` produces a refinement **by-passing** the predicate
// checking, i.e. with *no* verification whatsover.
//
// Use it cautiously, i.e. only when absolutely sure it's fine.
static constexpr auto unverified_make(T value) -> refinement {
return refinement{std::move(value)};
}

private:
explicit constexpr refinement(T val) : value{std::move(val)} {}
};
Expand Down
15 changes: 15 additions & 0 deletions test/refined_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ TEST_CASE(
STATIC_REQUIRE(even::make(3) == std::nullopt);
}

TEST_CASE("Two refinement types with same ground types and nominally defined "
"subtyping are implicily convertible",
"[predicate_subtyping]") {
using even_lt_10 =
refined::refinement<int, [](auto const x) { return x < 10; }, even>;

STATIC_REQUIRE(std::is_constructible_v<even, even_lt_10>);

constexpr even valid = *even_lt_10::make(8);
STATIC_REQUIRE(valid.value == 8);

constexpr std::optional<even> invalid = even_lt_10::make(10);
STATIC_REQUIRE(invalid == std::nullopt);
}

TEST_CASE("A to_exception policy should throw on invalid argument",
"[error_policy][to_exception]") {

Expand Down