From 4c68bb54e93164aafd07be5575fb9c38c86b6fb3 Mon Sep 17 00:00:00 2001 From: Rafael Varago Date: Sat, 29 Mar 2025 13:42:18 +0100 Subject: [PATCH] Allow "sub-type"-like conversions between refinements By enforcing that bases predicates were satisfied by taking a logical-conjuction of all of them, we can safely upcast via an implicit conversion. --- README.md | 18 +++++++++----- include/rvarago/refined.hpp | 47 ++++++++++++++++++++++++++++++------- test/refined_test.cpp | 15 ++++++++++++ 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b0dbe4b..baa7373 100644 --- a/README.md +++ b/README.md @@ -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. @@ -21,23 +21,29 @@ 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; - + +// refinements can be further refined, e.g. all x such that x is even and x < 10. +using even_lt_10 = + rvarago::refined::refinement; + 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 e = even::make(x); e) { + // the default error policy gives an std::optional back. + if (std::optional e = even_lt_10::make(x); e) { do_something(*e); } @@ -45,9 +51,9 @@ int main() { } ``` -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` or define a whole user-provided policy. ## Requirements diff --git a/include/rvarago/refined.hpp b/include/rvarago/refined.hpp index d1dd685..38155cd 100644 --- a/include/rvarago/refined.hpp +++ b/include/rvarago/refined.hpp @@ -68,29 +68,60 @@ struct to_exception { } // namespace error -// `refinement` constraints values `t: T` where `Pred(t)` holds. -template auto Pred> struct refinement { +// Reusable predicates. +namespace preds { + +// Assumes all values of `T` are valid. +template constexpr auto unconstrained(T const &) -> bool { + return true; +} +} // namespace preds + +template auto Pred, typename... Bases> + requires(std::is_same_v && ...) +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 + requires(std::is_same_v || ...) + 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 > Policy = error::to_optional> + template < + error::policy> Policy = error::to_optional> static constexpr auto make(T value, Policy policy = {}) - -> Policy::template wrapper_type> { - if (std::invoke(std::move(Pred), value)) { - return policy.template ok>( - {refinement{std::move(value)}}); + -> Policy::template wrapper_type> { + if (check(value) && (Bases::check(value) && ...)) { + return policy.template ok>( + {refinement{std::move(value)}}); } else { - return policy.template err>(); + return policy.template err>(); } } + // `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)} {} }; diff --git a/test/refined_test.cpp b/test/refined_test.cpp index 11135c6..56b1f45 100644 --- a/test/refined_test.cpp +++ b/test/refined_test.cpp @@ -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; + + STATIC_REQUIRE(std::is_constructible_v); + + constexpr even valid = *even_lt_10::make(8); + STATIC_REQUIRE(valid.value == 8); + + constexpr std::optional 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]") {