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: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

Design history of libfn, newest first. The living documents — [README.md](README.md), [CONTRIBUTING.md](CONTRIBUTING.md), [docs/](docs/) — describe only the present state of the design; when a decision makes an earlier idea obsolete, this file is where the transition is recorded and explained.

## Void expected monadic noexcept includes callback-result construction — 17 October 2026

The void specialization of `pfn::expected` includes callback-result construction in the conditional `noexcept` of `and_then` and `or_else`. `and_then` also retains its error-construction check. `or_else` has no unchanged value to construct. Throwing copies and moves of reference-returned results propagate, and immovable prvalues retain guaranteed copy elision.

Tests exercise all source cv/ref categories, reference-result copies and moves, and prvalues for both operations. No `fn` wrapper is changed.

## Object expected monadic noexcept includes callback-result construction — 17 October 2026

For non-void `pfn::expected<T, E>`, `and_then` and `or_else` include copying or moving a callback result returned by reference in their conditional `noexcept`. They retain the check for constructing the unchanged error or value. Throwing result construction propagates; immovable prvalue results remain supported through guaranteed copy elision. Constraints and invalid-callback diagnostics are unchanged.

Tests cover result-reference and source cv/ref categories, throwing copies and moves, non-throwing controls, and immovable prvalues.

## Optional monadic noexcept includes callback-result construction — 17 October 2026

`pfn::optional` accounts for constructing the returned optional in the conditional `noexcept` of `and_then` and `or_else`, for owning and reference specializations. A non-throwing callback can return a reference to an optional whose copy or move throws; that exception propagates instead of terminating. The specified constraints and invalid-callback diagnostics are unchanged. Prvalue results retain guaranteed copy elision, including immovable payloads.

Tests cover throwing copies and moves, non-throwing reference results, and immovable prvalues. The `fn` monadic wrappers remain separate work.

## Void expected monadic overloads follow the draft — 17 October 2026

The void specialization of `pfn::expected` uses body-deduced `auto` returns for all four monadic operations. `and_then` and `transform` retain their error-construction constraints; `or_else` and `transform_error` have no constructibility constraint. Invalid callbacks cause hard errors instead of disappearing through the helper's trailing return type.
Expand Down
4 changes: 3 additions & 1 deletion TYPE_ALGEBRA.md
Original file line number Diff line number Diff line change
Expand Up @@ -1072,10 +1072,12 @@ The library is divided into layers:
- `pfn` (Polyfill fn) is the standards-facing layer. It provides `std::optional` and `std::expected` in their C++26 shape — monadic member functions, `optional<T&>`, range support — plus smaller utilities such as `std::invoke_r` and `std::unreachable`, all available to a C++20 compiler.
- `fn` is the strict extension layer. It introduces the `pack`/`copack` algebra, multidispatch, graded errors, `choice`, `just`, the pipeline functors, and the composition operators `&` and `|`.

Every `fn` type with a `pfn` counterpart is a strict superset of it: switching a valid program from `pfn` to `fn` changes neither compilation nor behaviour.
Every `fn` type with a `pfn` counterpart is a strict superset of it: switching a valid program from `pfn` to `fn` changes neither compilation nor behaviour. A known exception is the monadic wrappers’ conditional `noexcept`: `fn` can still terminate when constructing a callback result throws, where the corrected `pfn` operation propagates the exception. The `fn` fixes are separate work ([#436](https://github.com/libfn/functional/issues/436)).

For an owning `pfn::optional<T>`, `transform` and `and_then` pass the payload with the source's cv/ref category. If the callback cannot accept that argument, the call is ill-formed; another cv/ref member overload is not a fallback. An `optional<T&>` passes its referent as `T&`, including through a temporary wrapper. Reference `and_then` also produces a hard error for an invalid invocation. Reference `transform` instead substitutes its explicit result type, so a non-invocable callback removes it from overload resolution. Both optional specializations constrain `or_else` on callback invocability; the owning specialization additionally requires copy or move construction for its respective overload. These rejection rules describe the `pfn` interface.

For `pfn::optional` and `pfn::expected`, the conditional `noexcept` on `and_then` and `or_else` accounts for invoking the callback and constructing the returned object, including copying or moving a callback result returned by reference. Prvalue results use guaranteed copy elision, so they need no copy or move constructor. For `optional`, `or_else` also accounts for copying or moving an engaged source; for `expected`, both operations account for constructing the unchanged value or error, where one exists.

For `pfn::expected<T, E>`, `and_then` and `transform` constrain construction of the unchanged error `E`. For non-void `T`, `or_else` and `transform_error` similarly constrain construction of `T`; the void specialization has no such constraint. These constraints can remove an overload and permit copying through another cv/ref overload. For callbacks with an argument, this can pass a const lvalue from an rvalue source. Once an overload is selected, an invalid callback is a hard error, including in a dependent `requires`-expression.

## Functional terminology
Expand Down
14 changes: 10 additions & 4 deletions include/pfn/expected.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,9 @@ template <class T, class E, class Policy> struct _expected_base {

template <typename Self, typename Fn>
static constexpr auto _and_then(Self &&self, Fn &&fn) //
noexcept(::std::is_nothrow_invocable_v<Fn, decltype(_expected_base::_value(FWD(self)))>
noexcept(::std::is_nothrow_invocable_r_v<
::std::remove_cvref_t<::std::invoke_result_t<Fn, decltype(_expected_base::_value(FWD(self)))>>, Fn,
decltype(_expected_base::_value(FWD(self)))>
&& ::std::is_nothrow_constructible_v<E, decltype(_expected_base::_error(FWD(self)))>)
requires(not ::std::is_void_v<T> && ::std::is_invocable_v<Fn, decltype(_expected_base::_value(FWD(self)))>
&& ::std::is_constructible_v<E, decltype(_expected_base::_error(FWD(self)))>)
Expand All @@ -868,7 +870,7 @@ template <class T, class E, class Policy> struct _expected_base {

template <typename Self, typename Fn>
static constexpr auto _and_then(Self &&self, Fn &&fn) //
noexcept(::std::is_nothrow_invocable_v<Fn>
noexcept(::std::is_nothrow_invocable_r_v<::std::remove_cvref_t<::std::invoke_result_t<Fn>>, Fn>
&& ::std::is_nothrow_constructible_v<E, decltype(_expected_base::_error(FWD(self)))>)
requires(::std::is_void_v<T> && ::std::is_invocable_v<Fn>
&& ::std::is_constructible_v<E, decltype(_expected_base::_error(FWD(self)))>)
Expand All @@ -884,7 +886,9 @@ template <class T, class E, class Policy> struct _expected_base {

template <typename Self, typename Fn>
static constexpr auto _or_else(Self &&self, Fn &&fn) //
noexcept(::std::is_nothrow_invocable_v<Fn, decltype(_expected_base::_error(FWD(self)))>
noexcept(::std::is_nothrow_invocable_r_v<
::std::remove_cvref_t<::std::invoke_result_t<Fn, decltype(_expected_base::_error(FWD(self)))>>, Fn,
decltype(_expected_base::_error(FWD(self)))>
&& ::std::is_nothrow_constructible_v<T, decltype(_expected_base::_value(FWD(self)))>)
requires(not ::std::is_void_v<T> && ::std::is_invocable_v<Fn, decltype(_expected_base::_error(FWD(self)))>
&& ::std::is_constructible_v<T, decltype(_expected_base::_value(FWD(self)))>)
Expand All @@ -900,7 +904,9 @@ template <class T, class E, class Policy> struct _expected_base {

template <typename Self, typename Fn>
static constexpr auto _or_else(Self &&self, Fn &&fn) //
noexcept(::std::is_nothrow_invocable_v<Fn, decltype(_expected_base::_error(FWD(self)))>)
noexcept(::std::is_nothrow_invocable_r_v<
::std::remove_cvref_t<::std::invoke_result_t<Fn, decltype(_expected_base::_error(FWD(self)))>>, Fn,
decltype(_expected_base::_error(FWD(self)))>)
requires(::std::is_void_v<T> && ::std::is_invocable_v<Fn, decltype(_expected_base::_error(FWD(self)))>)
{
using result_t = ::std::remove_cvref_t<::std::invoke_result_t<Fn, decltype(_expected_base::_error(FWD(self)))>>;
Expand Down
11 changes: 7 additions & 4 deletions include/pfn/optional.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,8 @@ template <class T, class Policy> struct _optional_base {
// through public API -- same reasoning as the _from_optional_t constructors above.
template <typename Self, typename Fn>
static constexpr auto _and_then(Self &&self, Fn &&fn) //
noexcept(::std::is_nothrow_invocable_v<Fn, decltype(*FWD(self))>)
noexcept(::std::is_nothrow_invocable_r_v<::std::remove_cvref_t<::std::invoke_result_t<Fn, decltype(*FWD(self))>>,
Fn, decltype(*FWD(self))>)
requires ::std::is_invocable_v<Fn, decltype(*FWD(self))>
{
using result_t = ::std::remove_cvref_t<::std::invoke_result_t<Fn, decltype(*FWD(self))>>;
Expand Down Expand Up @@ -724,7 +725,7 @@ template <class T, class Policy> struct _optional_base {

template <typename Self, typename Fn>
static constexpr auto _or_else(Self &&self, Fn &&fn) //
noexcept(::std::is_nothrow_invocable_v<Fn>
noexcept(::std::is_nothrow_invocable_r_v<typename Policy::template type<T>, Fn>
&& ::std::is_nothrow_constructible_v<typename Policy::template type<T>, Self>)
requires(::std::is_invocable_v<Fn> && ::std::is_constructible_v<typename Policy::template type<T>, Self>)
{
Expand Down Expand Up @@ -877,7 +878,8 @@ template <class T, class Policy> struct _optional_base<T &, Policy> {
// T& (const on the optional does not propagate to the referent), so Self only says which
// public overload delegated here.
template <typename Self, typename Fn>
static constexpr auto _and_then(Self &&self, Fn &&fn) noexcept(::std::is_nothrow_invocable_v<Fn, T &>)
static constexpr auto _and_then(Self &&self, Fn &&fn) noexcept(
::std::is_nothrow_invocable_r_v<::std::remove_cvref_t<::std::invoke_result_t<Fn, T &>>, Fn, T &>)
requires ::std::is_invocable_v<Fn, T &>
{
using result_t = ::std::remove_cvref_t<::std::invoke_result_t<Fn, T &>>;
Expand Down Expand Up @@ -905,7 +907,8 @@ template <class T, class Policy> struct _optional_base<T &, Policy> {
}

template <typename Self, typename Fn>
static constexpr auto _or_else(Self &&self, Fn &&fn) noexcept(::std::is_nothrow_invocable_v<Fn>)
static constexpr auto
_or_else(Self &&self, Fn &&fn) noexcept(::std::is_nothrow_invocable_r_v<typename Policy::template type<T &>, Fn>)
requires ::std::is_invocable_v<Fn>
{
using result_t = typename Policy::template type<T &>;
Expand Down
151 changes: 151 additions & 0 deletions tests/pfn/expected.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,141 @@ concept has_transform = requires(O &&o, F &&f) { std::forward<O>(o).transform(st
template <class O, class F>
concept has_transform_error = requires(O &&o, F &&f) { std::forward<O>(o).transform_error(std::forward<F>(f)); };

// Result-construction noexcept tests exclude the fn wrappers, whose fixes are separate work (#436).
struct monadic_result_error {};

template <bool CopyNoexcept, bool MoveNoexcept> struct monadic_result {
int value;
bool fail;
constexpr explicit monadic_result(int v, bool throws = false) noexcept : value(v), fail(throws) {}
constexpr monadic_result(monadic_result const &other) noexcept(CopyNoexcept) : value(other.value), fail(other.fail)
{
if constexpr (not CopyNoexcept)
if (fail)
throw monadic_result_error{};
}
constexpr monadic_result(monadic_result &&other) noexcept(MoveNoexcept) : value(other.value), fail(other.fail)
{
if constexpr (not MoveNoexcept)
if (fail)
throw monadic_result_error{};
}
};

template <bool Recover, class S, class F>
requires(not Recover)
constexpr auto monadic_operation(S &&source,
F &&fn) noexcept(noexcept(std::forward<S>(source).and_then(std::forward<F>(fn))))
{
return std::forward<S>(source).and_then(std::forward<F>(fn));
}

template <bool Recover, class S, class F>
requires Recover
constexpr auto monadic_operation(S &&source,
F &&fn) noexcept(noexcept(std::forward<S>(source).or_else(std::forward<F>(fn))))
{
return std::forward<S>(source).or_else(std::forward<F>(fn));
}

template <bool Error, class R, class... Args> constexpr R make_monadic_result(Args &&...args)
{
if constexpr (Error)
return R(unexpect, std::forward<Args>(args)...);
else
return R(std::in_place, std::forward<Args>(args)...);
}

template <class O, bool Recover, bool Extension> void check_monadic_result_references()
{
auto check_source = []<class S> {
auto check_result = []<class Ref, bool Nothrow> {
using R = std::remove_cvref_t<Ref>;
static_assert(not Extension || std::is_nothrow_constructible_v<R, Ref> == Nothrow);
constexpr auto read_result = [](R const &result) {
if constexpr (Recover)
return result.error().value;
else
return result->value;
};
constexpr auto make_source = []<bool Active> {
if constexpr (Recover == Active)
return O(unexpect, 5);
else
return O();
};
constexpr auto succeeds = [=] {
O source = make_source.template operator()<true>();
O bypass = make_source.template operator()<false>();
R returned = make_monadic_result<Recover, R>(7);
auto fn = [&returned](auto &&...) noexcept -> Ref { return static_cast<Ref>(returned); };
static_assert(not Extension || noexcept(monadic_operation<Recover>(std::declval<S>(), fn)) == Nothrow);
auto result = monadic_operation<Recover>(static_cast<S>(source), fn);
auto unchanged = monadic_operation<Recover>(static_cast<S>(bypass), fn);
return result.has_value() != Recover && read_result(result) == 7 && read_result(returned) == 7
&& source.has_value() != Recover && unchanged.has_value() == Recover;
};
static_assert(succeeds());
CHECK(succeeds());
if constexpr (not Nothrow) {
O source = make_source.template operator()<true>();
O bypass = make_source.template operator()<false>();
R returned = make_monadic_result<Recover, R>(7, true);
auto fn = [&returned](auto &&...) noexcept -> Ref { return static_cast<Ref>(returned); };
CHECK_THROWS_AS(monadic_operation<Recover>(static_cast<S>(source), fn), monadic_result_error);
CHECK(source.has_value() != Recover);
if constexpr (Recover)
CHECK(source.error() == 5);
else if constexpr (not std::is_void_v<typename O::value_type>)
CHECK(*source == 0);
CHECK(read_result(returned) == 7);
CHECK(monadic_operation<Recover>(static_cast<S>(bypass), fn).has_value() == Recover);
}
};
using Copy = std::conditional_t<Recover, expected<typename O::value_type, monadic_result<false, true>>,
expected<monadic_result<false, true>, int>>;
using Move = std::conditional_t<Recover, expected<typename O::value_type, monadic_result<true, false>>,
expected<monadic_result<true, false>, int>>;
check_result.template operator()<Copy &, false>();
check_result.template operator()<Copy const &, false>();
check_result.template operator()<Copy &&, true>();
check_result.template operator()<Copy const &&, false>();
check_result.template operator()<Move &, true>();
check_result.template operator()<Move &&, false>();
};
check_source.template operator()<O &>();
check_source.template operator()<O const &>();
check_source.template operator()<O &&>();
check_source.template operator()<O const &&>();
}

template <class O, bool Recover, bool Extension> void check_monadic_prvalue()
{
constexpr auto check = [] {
using R = std::conditional_t<Recover, expected<typename O::value_type, helper_immovable>,
expected<helper_immovable, int>>;
static_assert(not std::is_copy_constructible_v<R> && not std::is_move_constructible_v<R>);
O source = [] {
if constexpr (Recover)
return O(unexpect, 5);
else
return O();
}();
auto fn = [](auto &&...) noexcept -> R { return make_monadic_result<Recover, R>(helper_list_t{}, 7); };
auto cfn = [](auto &&...) noexcept -> R const { return make_monadic_result<Recover, R>(helper_list_t{}, 9); };
static_assert(not Extension || noexcept(monadic_operation<Recover>(source, fn)));
static_assert(not Extension || noexcept(monadic_operation<Recover>(source, cfn)));
auto result = monadic_operation<Recover>(source, fn);
auto const_result = monadic_operation<Recover>(std::move(source), cfn);
if constexpr (Recover)
return not result && result.error().v == 7 && not const_result && const_result.error().v == 9;
else
return result && result->v == 7 && const_result && const_result->v == 9;
};
static_assert(check());
CHECK(check());
}

} // namespace

TEST_CASE("bad_expected_access", "[expected][polyfill][bad_expected_access]")
Expand Down Expand Up @@ -3409,6 +3544,10 @@ TEST_CASE("expected non void", "[expected][polyfill]")
{
SECTION("and_then")
{
#if !defined(PFN_TEST_NESTED) || defined(PFN_TEST_VALIDATION)
SECTION("result references") { check_monadic_result_references<expected<int, int>, false, extension>(); }
SECTION("immovable prvalue") { check_monadic_prvalue<expected<int, int>, false, extension>(); }
#endif
SECTION("constructibility constraints")
{
using O = expected<int, monadic_move_only>;
Expand Down Expand Up @@ -3540,6 +3679,10 @@ TEST_CASE("expected non void", "[expected][polyfill]")

SECTION("or_else")
{
#if !defined(PFN_TEST_NESTED) || defined(PFN_TEST_VALIDATION)
SECTION("result references") { check_monadic_result_references<expected<int, int>, true, extension>(); }
SECTION("immovable prvalue") { check_monadic_prvalue<expected<int, int>, true, extension>(); }
#endif
SECTION("constructibility constraints")
{
using O = expected<monadic_move_only, int>;
Expand Down Expand Up @@ -5932,6 +6075,10 @@ TEST_CASE("expected void", "[expected_void][polyfill]")
{
SECTION("and_then")
{
#if !defined(PFN_TEST_NESTED) || defined(PFN_TEST_VALIDATION)
SECTION("result references") { check_monadic_result_references<expected<void, int>, false, extension>(); }
SECTION("immovable prvalue") { check_monadic_prvalue<expected<void, int>, false, extension>(); }
#endif
SECTION("constructibility constraints")
{
using O = expected<void, monadic_move_only>;
Expand Down Expand Up @@ -6046,6 +6193,10 @@ TEST_CASE("expected void", "[expected_void][polyfill]")

SECTION("or_else")
{
#if !defined(PFN_TEST_NESTED) || defined(PFN_TEST_VALIDATION)
SECTION("result references") { check_monadic_result_references<expected<void, int>, true, extension>(); }
SECTION("immovable prvalue") { check_monadic_prvalue<expected<void, int>, true, extension>(); }
#endif
SECTION("immovable error")
{
using O = expected<void, helper_immovable>;
Expand Down
Loading
Loading