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

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

Compile-failure tests cover nullary callback detection and error callback cv/ref fallback. Preservation tests cover valid copying and error transformations with an immovable original error, including all four source cv/ref categories. Conditional `noexcept` is unchanged.

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

The non-void `pfn::expected<T, E>` monadic members use body-deduced `auto` returns and expose the draft's constructibility constraints. Previously helper-based trailing returns also removed overloads for invalid callbacks, allowing a different cv/ref callback to run or a dependent `requires`-expression to return false. Invalid callbacks now cause hard errors after overload selection. Overloads whose unchanged side cannot be constructed still drop out, preserving valid copy fallback.

Compile-failure tests cover all four operations and cv/ref fallback directions, with positive controls and independently checked rejection fixtures. Runtime and constant-evaluation tests preserve constructibility-based fallback. Conditional `noexcept` is unchanged.

## Reference optional monadic declarations match the draft — 17 October 2026

`pfn::optional<T&>::and_then` uses body-deduced `auto`, so a non-invocable callback causes a hard error even in a dependent `requires`-expression. Previously its helper-based trailing return type treated that error as substitution failure. Reference `transform` retains substitution failure through the draft's explicit `optional<remove_cv_t<invoke_result_t<F, T&>>>` return type. Both specializations spell `or_else` with its fixed `optional` return type and keep its specified constraints. Valid calls retain conditional `noexcept`.

Paired compile-failure tests cover reference `and_then` and owning `and_then`/`transform` cv/ref fallback. Each rejection has a normal-build positive control using the same callback fixture.

## `pfn::optional` preserves the selected monadic overload — 17 October 2026

Owning `pfn::optional<T>::transform` and `and_then` reject callbacks that cannot accept the selected payload cv/ref category, as the standard requires. Previously, substitution failure could remove the selected member and allow a different cv/ref overload to run. A callback that explicitly deleted its rvalue overload could consequently borrow from a temporary optional through its lvalue overload and leave a dangling reference.

The members use unconstrained `auto` return types, so invalid invocations produce hard errors rather than falling back. Valid calls retain their conditional `noexcept`, and reference optionals still pass their referents as lvalues.

## Conan selects the C++26 mode with an option — 13 October 2026

The Conan recipe replaces its `fn_cxx26` component with a `cxx26` option, following the review of the Conan Center recipe. CMakeDeps links every component into the `libfn::libfn` aggregate, so linking that target selected `LIBFN_CXX26` and C++26 without the consumer asking. The option makes mode selection explicit; consumers that exchange `fn` types must agree on it because the modes use different ABI namespaces. The profile's `compiler.cppstd` setting now selects C++26 or newer, replacing the CMake build module that supplied that requirement. When `cxx26=True`, `libfn::fn_cxx26` is an alias of `libfn::fn`.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Match the assertion to the tested property:
* **Conditional Specifications**: Pair negative assertions with their positive converses (e.g., pair `static_assert(not noexcept(expr))` with a witness for which it is `noexcept`). This proves that the specification is conditional rather than unconditionally false. Apply the same positive-control discipline to negative constraint or viability probes.
* **Substitution Contexts**: Ensure expressions in negative viability probes are dependent on a template parameter. A `requires`-expression over concrete types is not a substitution context, meaning an invalid requirement triggers a hard error rather than a SFINAE failure. Enclose the dependent probe in a generic lambda or a type-keyed concept to yield `false` on invalid substitution.
* **Exception Fixtures**: Select exception-proving fixtures carefully. The library's `helper_t` implements a separate constructor for each value category, and its non-const-lvalue copy constructor is always `noexcept`. A `helper_t` configured to throw is therefore not throwing for every value category, which can hide incorrect `noexcept` specifications. Use a simple, local type if the test requires every relevant constructor to be potentially throwing.
* **Expected Failures**: The suite cannot test runtime crashes or compilation failures. Record these untested cases beside their positive controls.
* **Expected Failures**: Compile-must-fail tests build excluded object targets through CTest with `WILL_FAIL`. Pair each rejection with a normal-build positive control using the same fixture and compiler configuration. Check rejection-fixture assumptions in a build that must succeed, with the rejected call omitted. The suite cannot test runtime crashes; record these untested cases beside their positive controls.

## Client code

Expand Down
4 changes: 4 additions & 0 deletions TYPE_ALGEBRA.md
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,10 @@ The library is divided into layers:

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.

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

For readers with a background in functional languages (like Haskell or OCaml), this table translates standard terminology to `libfn`'s C++ vocabulary:
Expand Down
58 changes: 26 additions & 32 deletions include/pfn/expected.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1415,118 +1415,119 @@ template <class T, class E> class expected : private detail::_expected_base<T, E
using _base::value_or;

// [expected.object.monadic], monadic operations
// Only the unchanged side constrains participation; callback errors must not select another cv/ref overload.
template <class F>
constexpr auto and_then(F &&f) & //
noexcept(noexcept(_base::_and_then(*this, FWD(f)))) // extension
-> decltype(_base::_and_then(*this, FWD(f)))
requires ::std::is_constructible_v<E, E &>
{
return _base::_and_then(*this, FWD(f));
}
template <class F>
constexpr auto and_then(F &&f) && //
noexcept(noexcept(_base::_and_then(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_and_then(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<E, E &&>
{
return _base::_and_then(::std::move(*this), FWD(f));
}
template <class F>
constexpr auto and_then(F &&f) const & //
noexcept(noexcept(_base::_and_then(*this, FWD(f)))) // extension
-> decltype(_base::_and_then(*this, FWD(f)))
requires ::std::is_constructible_v<E, E const &>
{
return _base::_and_then(*this, FWD(f));
}
template <class F>
constexpr auto and_then(F &&f) const && //
noexcept(noexcept(_base::_and_then(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_and_then(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<E, E const &&>
{
return _base::_and_then(::std::move(*this), FWD(f));
}

template <class F>
constexpr auto or_else(F &&f) & //
noexcept(noexcept(_base::_or_else(*this, FWD(f)))) // extension
-> decltype(_base::_or_else(*this, FWD(f)))
requires ::std::is_constructible_v<T, T &>
{
return _base::_or_else(*this, FWD(f));
}
template <class F>
constexpr auto or_else(F &&f) && //
noexcept(noexcept(_base::_or_else(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_or_else(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<T, T &&>
{
return _base::_or_else(::std::move(*this), FWD(f));
}
template <class F>
constexpr auto or_else(F &&f) const & //
noexcept(noexcept(_base::_or_else(*this, FWD(f)))) // extension
-> decltype(_base::_or_else(*this, FWD(f)))
requires ::std::is_constructible_v<T, T const &>
{
return _base::_or_else(*this, FWD(f));
}
template <class F>
constexpr auto or_else(F &&f) const && //
noexcept(noexcept(_base::_or_else(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_or_else(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<T, T const &&>
{
return _base::_or_else(::std::move(*this), FWD(f));
}

template <class F>
constexpr auto transform(F &&f) & //
noexcept(noexcept(_base::_transform(*this, FWD(f)))) // extension
-> decltype(_base::_transform(*this, FWD(f)))
requires ::std::is_constructible_v<E, E &>
{
return _base::_transform(*this, FWD(f));
}
template <class F>
constexpr auto transform(F &&f) && //
noexcept(noexcept(_base::_transform(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_transform(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<E, E &&>
{
return _base::_transform(::std::move(*this), FWD(f));
}
template <class F>
constexpr auto transform(F &&f) const & //
noexcept(noexcept(_base::_transform(*this, FWD(f)))) // extension
-> decltype(_base::_transform(*this, FWD(f)))
requires ::std::is_constructible_v<E, E const &>
{
return _base::_transform(*this, FWD(f));
}
template <class F>
constexpr auto transform(F &&f) const && //
noexcept(noexcept(_base::_transform(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_transform(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<E, E const &&>
{
return _base::_transform(::std::move(*this), FWD(f));
}

template <class F>
constexpr auto transform_error(F &&f) & //
noexcept(noexcept(_base::_transform_error(*this, FWD(f)))) // extension
-> decltype(_base::_transform_error(*this, FWD(f)))
requires ::std::is_constructible_v<T, T &>
{
return _base::_transform_error(*this, FWD(f));
}
template <class F>
constexpr auto transform_error(F &&f) && //
noexcept(noexcept(_base::_transform_error(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_transform_error(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<T, T &&>
{
return _base::_transform_error(::std::move(*this), FWD(f));
}
template <class F>
constexpr auto transform_error(F &&f) const & //
noexcept(noexcept(_base::_transform_error(*this, FWD(f)))) // extension
-> decltype(_base::_transform_error(*this, FWD(f)))
requires ::std::is_constructible_v<T, T const &>
{
return _base::_transform_error(*this, FWD(f));
}
template <class F>
constexpr auto transform_error(F &&f) const && //
noexcept(noexcept(_base::_transform_error(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_transform_error(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<T, T const &&>
{
return _base::_transform_error(::std::move(*this), FWD(f));
}
Expand Down Expand Up @@ -1718,118 +1719,111 @@ template <class E> class expected<void, E> : private detail::_expected_base<void
using _base::value;

// [expected.void.monadic], monadic operations
// Keep callback errors in return deduction, separate from the specified error-construction constraints.
template <class F>
constexpr auto and_then(F &&f) & //
noexcept(noexcept(_base::_and_then(*this, FWD(f)))) // extension
-> decltype(_base::_and_then(*this, FWD(f)))
requires ::std::is_constructible_v<E, E &>
{
return _base::_and_then(*this, FWD(f));
}
template <class F>
constexpr auto and_then(F &&f) && //
noexcept(noexcept(_base::_and_then(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_and_then(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<E, E &&>
{
return _base::_and_then(::std::move(*this), FWD(f));
}
template <class F>
constexpr auto and_then(F &&f) const & //
noexcept(noexcept(_base::_and_then(*this, FWD(f)))) // extension
-> decltype(_base::_and_then(*this, FWD(f)))
requires ::std::is_constructible_v<E, E const &>
{
return _base::_and_then(*this, FWD(f));
}
template <class F>
constexpr auto and_then(F &&f) const && //
noexcept(noexcept(_base::_and_then(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_and_then(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<E, E const &&>
{
return _base::_and_then(::std::move(*this), FWD(f));
}

template <class F>
constexpr auto or_else(F &&f) & //
noexcept(noexcept(_base::_or_else(*this, FWD(f)))) // extension
-> decltype(_base::_or_else(*this, FWD(f)))
{
return _base::_or_else(*this, FWD(f));
}
template <class F>
constexpr auto or_else(F &&f) && //
noexcept(noexcept(_base::_or_else(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_or_else(::std::move(*this), FWD(f)))
{
return _base::_or_else(::std::move(*this), FWD(f));
}
template <class F>
constexpr auto or_else(F &&f) const & //
noexcept(noexcept(_base::_or_else(*this, FWD(f)))) // extension
-> decltype(_base::_or_else(*this, FWD(f)))
{
return _base::_or_else(*this, FWD(f));
}
template <class F>
constexpr auto or_else(F &&f) const && //
noexcept(noexcept(_base::_or_else(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_or_else(::std::move(*this), FWD(f)))
{
return _base::_or_else(::std::move(*this), FWD(f));
}

template <class F>
constexpr auto transform(F &&f) & //
noexcept(noexcept(_base::_transform(*this, FWD(f)))) // extension
-> decltype(_base::_transform(*this, FWD(f)))
requires ::std::is_constructible_v<E, E &>
{
return _base::_transform(*this, FWD(f));
}
template <class F>
constexpr auto transform(F &&f) && //
noexcept(noexcept(_base::_transform(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_transform(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<E, E &&>
{
return _base::_transform(::std::move(*this), FWD(f));
}
template <class F>
constexpr auto transform(F &&f) const & //
noexcept(noexcept(_base::_transform(*this, FWD(f)))) // extension
-> decltype(_base::_transform(*this, FWD(f)))
requires ::std::is_constructible_v<E, E const &>
{
return _base::_transform(*this, FWD(f));
}
template <class F>
constexpr auto transform(F &&f) const && //
noexcept(noexcept(_base::_transform(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_transform(::std::move(*this), FWD(f)))
requires ::std::is_constructible_v<E, E const &&>
{
return _base::_transform(::std::move(*this), FWD(f));
}

template <class F>
constexpr auto transform_error(F &&f) & //
noexcept(noexcept(_base::_transform_error(*this, FWD(f)))) // extension
-> decltype(_base::_transform_error(*this, FWD(f)))
{
return _base::_transform_error(*this, FWD(f));
}
template <class F>
constexpr auto transform_error(F &&f) && //
noexcept(noexcept(_base::_transform_error(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_transform_error(::std::move(*this), FWD(f)))
{
return _base::_transform_error(::std::move(*this), FWD(f));
}
template <class F>
constexpr auto transform_error(F &&f) const & //
noexcept(noexcept(_base::_transform_error(*this, FWD(f)))) // extension
-> decltype(_base::_transform_error(*this, FWD(f)))
{
return _base::_transform_error(*this, FWD(f));
}
template <class F>
constexpr auto transform_error(F &&f) const && //
noexcept(noexcept(_base::_transform_error(::std::move(*this), FWD(f)))) // extension
-> decltype(_base::_transform_error(::std::move(*this), FWD(f)))
{
return _base::_transform_error(::std::move(*this), FWD(f));
}
Expand Down
Loading
Loading