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
20 changes: 20 additions & 0 deletions include/fn/concepts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@

namespace fn {

namespace detail {
// A verb that returns a fresh monad BY VALUE relocates what the source carries into it, in the value
// category the source is piped in - an lvalue is copied, an rvalue moved. So the question is not
// whether the carried type is movable: `is_move_constructible_v` would accept a move-only value
// piped as an lvalue, which the body then cannot copy. It is whether the result can be built from
// what the body actually reaches for, which is what these ask - and what the verbs' noexcept specs
// weigh, one asking "can it", the other "can it throw".
template <typename V>
concept _relocatable_value // `type{::std::in_place, FWD(v).value()}`
= ::std::is_constructible_v<::std::remove_cvref_t<V>, ::std::in_place_t, decltype(::std::declval<V>().value())>;

template <typename V>
concept _relocatable_error // `type{::fn::unexpect, FWD(v).error()}`
= ::std::is_constructible_v<::std::remove_cvref_t<V>, ::fn::unexpect_t, decltype(::std::declval<V>().error())>;

template <typename V>
concept _relocatable // `return FWD(v);` - the whole monad, hence both sides
= ::std::is_constructible_v<::std::remove_cvref_t<V>, V>;
} // namespace detail

/**
* @brief TODO
* @note `same_kind` is a fundamental concept in category theory; it allows
Expand Down
3 changes: 3 additions & 0 deletions include/fn/fail.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#ifndef INCLUDE_FN_FAIL
#define INCLUDE_FN_FAIL

#include <fn/concepts.hpp>
#include <fn/functor.hpp>
#include <fn/optional.hpp>

Expand All @@ -27,8 +28,10 @@ concept invocable_fail //
{
::fn::invoke(FWD(fn), FWD(v).value())
} -> ::std::convertible_to<typename ::std::remove_cvref_t<V>::error_type>;
requires detail::_relocatable_error<V>; // the error branch carries the existing error over
}) || (some_expected_void<V> && requires(Fn &&fn) {
{ ::fn::invoke(FWD(fn)) } -> ::std::convertible_to<typename ::std::remove_cvref_t<V>::error_type>;
requires detail::_relocatable_error<V>;
}) || (some_optional<V> && requires(Fn &&fn, V &&v) {
{ ::fn::invoke(FWD(fn), FWD(v).value()) } -> ::std::same_as<void>;
});
Expand Down
5 changes: 5 additions & 0 deletions include/fn/filter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,23 @@ namespace fn {
* @tparam Err The error handler
* @tparam V The monadic type
*/
// A rejected operand is returned whole (`return FWD(v)`), and a kept one is rebuilt around its
// existing value - so both sides must survive the trip.
template <typename Pred, typename Err, typename V>
concept invocable_filter //
= (some_expected_non_void<V> && requires(Pred &&pred, Err &&on_err, V &&v) {
{ ::fn::invoke(FWD(pred), ::std::as_const(v).value()) } -> convertible_to_bool;
{
::fn::invoke(FWD(on_err), FWD(v).value())
} -> ::std::convertible_to<typename ::std::remove_cvref_t<V>::error_type>;
requires detail::_relocatable<V> && detail::_relocatable_value<V>;
}) || (some_expected_void<V> && requires(Pred &&pred, Err &&on_err, V &&v) {
{ ::fn::invoke(FWD(pred)) } -> convertible_to_bool;
{ ::fn::invoke(FWD(on_err)) } -> ::std::convertible_to<typename ::std::remove_cvref_t<V>::error_type>;
requires detail::_relocatable<V>;
}) || (some_optional<V> && ::std::same_as<Err, void> && requires(Pred &&pred, V &&v) {
{ ::fn::invoke(FWD(pred), ::std::as_const(v).value()) } -> convertible_to_bool;
requires detail::_relocatable<V> && detail::_relocatable_value<V>;
});

/**
Expand Down
11 changes: 11 additions & 0 deletions include/fn/recover.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#ifndef INCLUDE_FN_RECOVER
#define INCLUDE_FN_RECOVER

#include <fn/concepts.hpp>
#include <fn/functional.hpp>
#include <fn/functor.hpp>

Expand All @@ -20,16 +21,26 @@ namespace fn {
* @tparam Fn TODO
* @tparam V TODO
*/
// The recovered value builds the RESULT, not merely its value type: for an `optional<T&>` those
// differ - the value type is the referent, which a prvalue can construct, but the result binds a
// reference to it, which a prvalue cannot. The success branch carries the existing value over, so
// that must survive the trip too.
template <typename Fn, typename V>
concept invocable_recover //
= (some_expected_non_void<V> && requires(Fn &&fn, V &&v) {
{
::fn::invoke(FWD(fn), FWD(v).error())
} -> ::std::convertible_to<typename ::std::remove_cvref_t<V>::value_type>;
requires ::std::is_constructible_v<::std::remove_cvref_t<V>, ::std::in_place_t,
decltype(::fn::invoke(FWD(fn), FWD(v).error()))>;
Comment thread
Bronek marked this conversation as resolved.
requires detail::_relocatable_value<V>;
}) || (some_expected_void<V> && requires(Fn &&fn, V &&v) {
{ ::fn::invoke(FWD(fn), FWD(v).error()) } -> ::std::same_as<void>;
}) || (some_optional<V> && requires(Fn &&fn, V &&v) {
{ ::fn::invoke(FWD(fn)) } -> ::std::convertible_to<typename ::std::remove_cvref_t<V>::value_type>;
requires ::std::is_constructible_v<::std::remove_cvref_t<V>, ::std::in_place_t,
decltype(::fn::invoke(FWD(fn)))>;
requires detail::_relocatable_value<V>;
});

/**
Expand Down
19 changes: 14 additions & 5 deletions include/fn/value_or.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@ namespace fn {
* @tparam V TODO
* @tparam Args TODO
*/
// The fallback builds the RESULT, not merely its value type: for an `optional<T&>` those differ -
// the value type is the referent, which a prvalue can construct, but the result binds a reference to
// it, which a prvalue cannot. And the existing value is carried over when there is one, so it must
// be able to survive that: an immovable value type would otherwise satisfy this and then fail inside
// the body.
template <typename V, typename... Args>
concept invocable_value_or //
= (some_expected_non_void<V> && ::std::is_constructible_v<typename ::std::remove_cvref_t<V>::value_type, Args...>)
|| (some_optional<V> && ::std::is_constructible_v<typename ::std::remove_cvref_t<V>::value_type, Args...>);
concept invocable_value_or //
= (some_expected_non_void<V> && ::std::is_constructible_v<::std::remove_cvref_t<V>, ::std::in_place_t, Args...> //
&& detail::_relocatable_value<V>)
|| (some_optional<V> && ::std::is_constructible_v<::std::remove_cvref_t<V>, ::std::in_place_t, Args...>
&& detail::_relocatable_value<V>);

/**
* @brief TODO
Expand Down Expand Up @@ -58,8 +65,10 @@ struct value_or_t::apply final {
// callable or_else receives is a lambda, which cannot be named in this specification.
template <some_monadic_type V, typename... Args>
[[nodiscard]] constexpr auto operator()(V &&v, Args &&...args) const //
noexcept(::std::is_nothrow_constructible_v<::std::remove_cvref_t<V>, ::std::in_place_t, Args...>
&& ::std::is_nothrow_constructible_v<::std::remove_cvref_t<V>, V>) -> ::std::remove_cvref_t<V>
noexcept(
Comment thread
Bronek marked this conversation as resolved.
::std::is_nothrow_constructible_v<::std::remove_cvref_t<V>, ::std::in_place_t, Args...>
&& ::std::is_nothrow_constructible_v<::std::remove_cvref_t<V>, ::std::in_place_t, decltype(FWD(v).value())>)
-> ::std::remove_cvref_t<V>
requires invocable_value_or<V &&, Args...>
{
using type = ::std::remove_cvref_t<V>;
Expand Down
25 changes: 25 additions & 0 deletions tests/fn/fail.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

#include "util/static_check.hpp"

#include <util/helper_types.hpp>

#include <fn/fail.hpp>

#include <catch2/catch_all.hpp>
Expand Down Expand Up @@ -422,3 +424,26 @@ TEST_CASE("fail noexcept", "[fail][noexcept]")
static_assert(not noexcept(std::declval<O &>() | fn::fail([](int) {})));
SUCCEED();
}

TEST_CASE("fail constraints", "[fail][constraints]")
{
using namespace fn;

// The error branch carries the existing error over, so it must be able to
constexpr auto from_value = [](int) -> int { return 1; }; // converts to either helper below
using immovable_t = fn::expected<int, helper_immovable>;
static_assert(std::is_constructible_v<immovable_t, fn::unexpect_t, int>);
static_assert(monadic_static_check<fail_t, immovable_t>::not_invocable_with_any(from_value));
static_assert(monadic_static_check<fail_t, fn::expected<void, helper_immovable>>::not_invocable_with_any(
[]() -> int { return 1; }));

// ... and a move-only error only where it can be moved out of
using is = monadic_static_check<fail_t, fn::expected<int, helper_move_only>>;
static_assert(is::invocable<rvalue, prvalue>(from_value)); // moved
static_assert(is::invocable<crvalue, cvalue>(from_value)); // const-moved
static_assert(is::not_invocable<lvalue, clvalue>(from_value)); // would have to copy

// An optional discards its value and returns nullopt, relocating nothing
static_assert(monadic_static_check<fail_t, fn::optional<helper_immovable>>::invocable_with_any([](auto const &) {}));
SUCCEED();
}
23 changes: 23 additions & 0 deletions tests/fn/filter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

#include "util/static_check.hpp"

#include <util/helper_types.hpp>

#include <fn/filter.hpp>

#include <catch2/catch_all.hpp>
Expand Down Expand Up @@ -674,3 +676,24 @@ TEST_CASE("filter noexcept", "[filter][noexcept]")
static_assert(not noexcept(std::declval<O &>() | fn::filter([](int) { return true; })));
SUCCEED();
}

TEST_CASE("filter constraints", "[filter][constraints]")
{
using namespace fn;

// A rejected operand is returned whole and a kept one is rebuilt around its existing value, so
// BOTH sides must survive the trip - an immovable value or error is rejected either way
constexpr auto keep = [](auto const &) -> bool { return true; };
constexpr auto on_err = [](auto const &) -> int { return 1; };
static_assert(
monadic_static_check<filter_t, fn::expected<helper_immovable, int>>::not_invocable_with_any(keep, on_err));
static_assert(monadic_static_check<filter_t, fn::expected<int, helper_immovable>>::not_invocable_with_any(
[](int) -> bool { return true; }, [](int) -> int { return 1; }));
static_assert(monadic_static_check<filter_t, fn::optional<helper_immovable>>::not_invocable_with_any(keep));

// ... and a move-only one only where it can be moved out of
using is = monadic_static_check<filter_t, fn::expected<helper_move_only, int>>;
static_assert(is::invocable<rvalue, prvalue>(keep, on_err)); // moved
static_assert(is::not_invocable<lvalue, clvalue>(keep, on_err)); // would have to copy
SUCCEED();
}
36 changes: 36 additions & 0 deletions tests/fn/recover.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

#include "util/static_check.hpp"

#include <util/helper_types.hpp>

#include <fn/functor.hpp>
#include <fn/recover.hpp>

Expand Down Expand Up @@ -281,3 +283,37 @@ TEST_CASE("recover noexcept", "[recover][noexcept]")
static_assert(not noexcept(std::declval<S const &>() | fn::recover([](int) noexcept { return std::string{}; })));
SUCCEED();
}

TEST_CASE("recover constraints", "[recover][constraints]")
{
using namespace fn;

// The success branch carries the existing value over, so it must be able to: an immovable value
// type must be dropped by the concept, not fail inside the body.
constexpr auto from_error = [](Error) -> int { return 1; }; // converts to either helper below
using immovable_t = fn::expected<helper_immovable, Error>;
static_assert(std::is_constructible_v<immovable_t, std::in_place_t, int>);
static_assert(monadic_static_check<recover_t, immovable_t>::not_invocable_with_any(from_error));

// A move-only value type is carried only where it can be moved out of - which is why the question
// is `is_constructible_v<T, decltype(carried)>` and not `is_move_constructible_v<T>`
using is = monadic_static_check<recover_t, fn::expected<helper_move_only, Error>>;
static_assert(is::invocable<rvalue, prvalue>(from_error)); // moved
static_assert(is::invocable<crvalue, cvalue>(from_error)); // const-moved
static_assert(is::not_invocable<lvalue, clvalue>(from_error)); // would have to copy

// A void-valued expected has no value to carry, so nothing constrains it
static_assert(monadic_static_check<recover_t, fn::expected<void, Error>>::invocable_with_any([](Error) {}));

// A reference optional binds its referent: the recovered value builds the RESULT, and a reference
// cannot bind to the prvalue a callback returns
using ref_t = fn::optional<int &>;
static_assert(std::is_constructible_v<ref_t::value_type, int>);
static_assert(not invocable_recover<decltype([]() -> int { return 1; }) &, ref_t &>);
static_assert(invocable_recover<decltype([]() -> int & {
static int i = 1;
return i;
}) &,
ref_t &>);
SUCCEED();
}
50 changes: 50 additions & 0 deletions tests/fn/value_or.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
// Distributed under the ISC License. See accompanying file LICENSE.md
// or copy at https://opensource.org/licenses/ISC

#include "util/static_check.hpp"

#include <fn/value_or.hpp>

#include <util/helper_types.hpp>
Expand All @@ -13,6 +15,8 @@
#include <string_view>
#include <utility>

using namespace util;

namespace {
struct Error final {
std::string what;
Expand Down Expand Up @@ -166,5 +170,51 @@ TEST_CASE("value_or noexcept", "[value_or][noexcept]")
// building the fallback value can throw
using S = fn::optional<std::string>;
static_assert(not noexcept(std::declval<S &>() | fn::value_or("x")));

// the untouched error is never relocated, so its throwing copy does not weigh
using X = fn::expected<int, std::string>;
static_assert(noexcept(std::declval<X &>() | fn::value_or(1)));

// the carried value is relocated in the operand's category, and that weighs
struct MoveNothrow {
MoveNothrow(int) noexcept {}
MoveNothrow(MoveNothrow const &) noexcept(false) {}
MoveNothrow(MoveNothrow &&) noexcept {}
};
using W = fn::expected<MoveNothrow, int>;
static_assert(not noexcept(std::declval<W &>() | fn::value_or(1))); // copies
static_assert(noexcept(std::declval<W &&>() | fn::value_or(1))); // moves
SUCCEED();
}

TEST_CASE("value_or constraints", "[value_or][constraints]")
{
using namespace fn;

// The result carries the existing value over, so it must be able to. An immovable value type can
// still be built in place - which is not the question - but never carried: the candidate must
// drop, not fail inside the body.
using immovable_t = fn::expected<helper_immovable, Error>;
static_assert(std::is_constructible_v<immovable_t, std::in_place_t, int>);
static_assert(monadic_static_check<value_or_t, immovable_t>::not_invocable_with_any(1));
static_assert(monadic_static_check<value_or_t, fn::optional<helper_immovable>>::not_invocable_with_any(1));

// A move-only value type is carried only where it can be moved out of. This is what makes the
// question `is_constructible_v<T, decltype(carried)>` and not `is_move_constructible_v<T>` - the
// latter would accept every category below, including the two that would have to copy.
using move_only_t = fn::expected<helper_move_only, Error>;
using is = monadic_static_check<value_or_t, move_only_t>;
static_assert(is::invocable<rvalue, prvalue>(1)); // moved
static_assert(is::invocable<crvalue, cvalue>(1)); // const-moved
static_assert(is::not_invocable<lvalue, clvalue>(1)); // would have to copy

// A reference optional binds its referent rather than carrying it - so an immovable referent is
// fine, but the fallback builds the RESULT, and a reference cannot bind to a prvalue. The value
// type alone cannot answer this: it is the referent, which a prvalue constructs happily.
using ref_t = fn::optional<int &>;
static_assert(std::is_constructible_v<ref_t::value_type, int>);
static_assert(not invocable_value_or<ref_t &, int>); // a temporary to bind to: rejected
static_assert(invocable_value_or<ref_t &, int &>);
static_assert(invocable_value_or<fn::optional<helper_immovable &> &, helper_immovable &>);
SUCCEED();
}