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

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.

## `transform` over a copack maps a `void` result to `pack<>` — 27 September 2026

`transform` over a copack represents `void` callback results as `pack<>`. Mixed `void` and `int` results yield `copack_for<pack<>, int>`; all-`void` results yield `copack<pack<>>`. This applies through `expected`, `optional` and `choice`, and to `transform_error` over a copack error. Plain error types still reject `void` results. Mutable operands can select a `void` overload where they previously fell back to a valued `const` overload.
Comment thread
Bronek marked this conversation as resolved.

Results are converted before spliced arguments expire, fixing a dangling reference. The `noexcept` specification includes the selected overload's explicit result conversion. Rvalues spliced before a `pack` argument are stored by value, fixing a compilation failure.

`expected<void, E>::copack_value()` lifts to `expected<copack<pack<>>, E>`.

## `and_then` joins `void` and valued branches through `pack<>` — 26 September 2026

`and_then` accepts branches returning both `void`-valued and valued `expected` results. For example, `A -> expected<void, E>` and `B -> expected<int, E>` join as `expected<copack_for<pack<>, int>, E>`; this previously failed to compile. An all-`void` join stays `void`.
Expand Down
17 changes: 13 additions & 4 deletions docs/reference/expected.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,15 @@ constexpr auto copack_value() const && -> decltype(auto);

:include-doxygen-doc-params: fn::expected::copack_value { args: "", title: "parameters" }

```cpp {title: "fn::expected< void, Err >::copack_value"}
constexpr auto copack_value() const & -> expected<copack<pack<>>, error_type>; // (1)
constexpr auto copack_value() && -> expected<copack<pack<>>, error_type>; // (2)
```

:include-doxygen-doc: fn::expected< void, Err >::copack_value { args: "" }

:include-doxygen-doc-params: fn::expected< void, Err >::copack_value { args: "", title: "parameters" }

## and_then {style: "api"}

```cpp {title: "fn::expected::and_then"}
Expand Down Expand Up @@ -517,10 +526,10 @@ constexpr auto copack_error(some_expected auto &&src) -> decltype(auto); // (1)
:include-doxygen-doc-params: fn::copack_error { args: "some_expected auto &&", title: "parameters" }

```cpp {title: "fn::copack_value"}
constexpr auto copack_value(some_expected_non_void auto &&src) -> decltype(auto); // (1)
constexpr auto copack_value(some_optional auto &&src) -> decltype(auto); // (2)
constexpr auto copack_value(some_expected auto &&src) -> decltype(auto); // (1)
constexpr auto copack_value(some_optional auto &&src) -> decltype(auto); // (2)
```

:include-doxygen-doc: fn::copack_value { args: "some_expected_non_void auto &&" }
:include-doxygen-doc: fn::copack_value { args: "some_expected auto &&" }

:include-doxygen-doc-params: fn::copack_value { args: "some_expected_non_void auto &&", title: "parameters" }
:include-doxygen-doc-params: fn::copack_value { args: "some_expected auto &&", title: "parameters" }
113 changes: 87 additions & 26 deletions include/fn/copack.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,28 +104,27 @@ template <template <typename...> typename Tpl, typename... Ts> struct normalized
};
} // namespace _collapsing_copack

// A result no copack can hold - void above all - must drop the caller's candidate, not explode:
// everything behind the gate (flattening, normalization, the copack they name) hard-errors OUTSIDE
// any immediate context. The false gate has no `type`, and the collapsing traits pass that absence
// through by inheritance, so it surfaces where a return type names `::type` - in the immediate
// context, as a clean substitution failure.
// Reject invalid alternatives before normalization, whose errors would escape SFINAE.
template <bool, template <typename...> typename Tpl, typename... Rs> struct _collapsing_copack_gate {};
template <template <typename...> typename Tpl, typename... Rs> struct _collapsing_copack_gate<true, Tpl, Rs...> {
using type = _collapsing_copack::normalized<Tpl, _collapsing_copack::flattened<Rs...>>::type;
};

template <typename R> constexpr inline bool _collapsible_result = some_copack<R> || _is_valid_copack_subtype<R>;

// A copack represents void success with pack<>.
template <typename V> using _sum_element_t = ::std::conditional_t<::std::is_void_v<V>, ::fn::pack<>, V>;

template <typename Fn, typename Self, typename T, typename... Args> struct _typelist_collapsing_copack;
template <typename Fn, typename Self, template <typename...> typename Tpl, typename... Ts, typename... Args>
struct _typelist_collapsing_copack<Fn, Self, Tpl<Ts...>, Args...>
: _collapsing_copack_gate<
(...
&& _collapsible_result<::std::remove_cvref_t<
typename ::fn::detail::_apply_result<Fn, apply_const_lvalue_t<Self, Ts>, Args...>::type>>),
&& _collapsible_result<_sum_element_t<::std::remove_cvref_t<
typename ::fn::detail::_apply_result<Fn, apply_const_lvalue_t<Self, Ts>, Args...>::type>>>),
Tpl,
::std::remove_cvref_t<
typename ::fn::detail::_apply_result<Fn, apply_const_lvalue_t<Self, Ts>, Args...>::type>...> {};
_sum_element_t<::std::remove_cvref_t<
typename ::fn::detail::_apply_result<Fn, apply_const_lvalue_t<Self, Ts>, Args...>::type>>...> {};

template <typename T, typename Fn, typename Self, typename... Args> struct _copack_apply_result final {
using type = T;
Expand All @@ -138,6 +137,56 @@ template <typename Fn, typename Self, typename... Args>
struct _copack_apply_result<_collapsing_copack_tag, Fn, Self, Args...> final
: _typelist_collapsing_copack<Fn, Self, ::std::remove_cvref_t<Self>, Args...> {};

// Convert results before spliced arguments expire. Keep callback invocability
// unchanged so the injector preserves overload selection.
template <typename To, typename Fn, typename... Args> struct _nothrow_copack_inject : ::std::false_type {};
template <typename To, typename Fn, typename... Args>
requires ::std::is_void_v<::std::invoke_result_t<Fn, Args...>>
struct _nothrow_copack_inject<To, Fn, Args...>
: ::std::bool_constant<
::std::is_nothrow_invocable_v<Fn, Args...> && ::std::is_nothrow_constructible_v<To, ::fn::pack<>>> {};
template <typename To, typename Fn, typename... Args>
requires(not ::std::is_void_v<::std::invoke_result_t<Fn, Args...>>)
&& requires { static_cast<To>(::std::invoke(::std::declval<Fn>(), ::std::declval<Args>()...)); }
struct _nothrow_copack_inject<To, Fn, Args...>
: ::std::bool_constant<noexcept(static_cast<To>(::std::invoke(::std::declval<Fn>(), ::std::declval<Args>()...)))> {
};

template <typename To, typename Fn> struct _copack_injector final {
Fn fn;

template <typename... Args>
constexpr auto operator()(Args &&...args) const noexcept(_nothrow_copack_inject<To, Fn, Args...>::value) -> To
requires ::std::is_invocable_v<Fn, Args...>
{
using result = ::std::invoke_result_t<Fn, Args...>;
if constexpr (::std::is_void_v<result>) {
::std::invoke(FWD(fn), FWD(args)...);
return To{_sum_element_t<result>{}}; // pack<>, spelled dependently: pack.hpp includes this header
} else
return static_cast<To>(::std::invoke(FWD(fn), FWD(args)...));
}
};

template <typename To, typename Fn> struct _copack_admission final {
template <typename... Args>
auto operator()(Args &&...) const -> To
requires(::std::is_void_v<::std::invoke_result_t<Fn, Args...>> && ::std::is_constructible_v<To, ::fn::pack<>>)
|| ((not ::std::is_void_v<::std::invoke_result_t<Fn, Args...>>) && ::std::is_invocable_r_v<To, Fn, Args...>)
{
::pfn::unreachable(); // LCOV_EXCL_LINE
}
};

template <typename To, typename Data, typename Fn, typename... Args>
requires _typelist_applicable_r<To, _copack_admission<To, Fn &&>, Data &&, Args &&...>
[[nodiscard]] constexpr auto _collapsing_apply(Data &&data, ::std::size_t index, Fn &&fn, Args &&...args) //
noexcept(_is_nothrow_rts_applicable<To, _copack_injector<To, Fn &&>, Data &&, Args &&...>) -> To
{
return apply_variadic_union<To, ::std::remove_cvref_t<Data>>(FWD(data), index, _copack_injector<To, Fn &&>{FWD(fn)},
FWD(args)...);
}

struct _joining_superset_tag final {};

// Bind joins the payloads of just results. A choice contributes its alternatives, an ordinary
Expand Down Expand Up @@ -1086,8 +1135,9 @@ struct copack<Ts...> {
* @brief Maps the alternatives, the branch results forming a new normalized copack
*
* The self-flattening map: the callable is dispatched exhaustively, and the branch results -
* heterogeneous types allowed, a copack result dissolving into the set - flatten, deduplicate
* and sort into the `copack_for` of them all.
* heterogeneous types allowed, a copack result dissolving into the set, a `void` one entering as
* `pack<>` - flatten, deduplicate and sort into the `copack_for` of them all. A `void` result
* needs `<fn/pack.hpp>`, which defines `pack`.
*
* @param fn Callable applied on the active alternative; `fn::overload` fuses arms into one
* @param args Additional arguments, appended after the alternative's content
Expand All @@ -1097,48 +1147,62 @@ struct copack<Ts...> {
[[nodiscard]] constexpr auto transform(Fn &&fn, Args &&...args) & noexcept(
detail::_is_nothrow_rts_applicable<
typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack &, Args &&...>::type,
Fn &&, copack &, Args &&...>) ->
detail::_copack_injector<
typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack &, Args &&...>::type,
Fn &&>,
copack &, Args &&...>) ->
typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack &, Args &&...>::type
requires typelist_applicable<Fn, copack &, Args &&...>
{
using type = detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack &, Args &&...>::type;
return detail::apply_variadic_union<type, data_t>(this->data, index, FWD(fn), FWD(args)...);
return detail::_collapsing_apply<type>(this->data, index, FWD(fn), FWD(args)...);
}

template <typename Fn, typename... Args>
[[nodiscard]] constexpr auto transform(Fn &&fn, Args &&...args) const & noexcept(
detail::_is_nothrow_rts_applicable<typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&,
copack const &, Args &&...>::type,
Fn &&, copack const &, Args &&...>) ->
detail::_is_nothrow_rts_applicable<
typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack const &,
Args &&...>::type,
detail::_copack_injector<typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&,
copack const &, Args &&...>::type,
Fn &&>,
copack const &, Args &&...>) ->
typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack const &, Args &&...>::type
requires typelist_applicable<Fn, copack const &, Args &&...>
{
using type = detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack const &, Args &&...>::type;
return detail::apply_variadic_union<type, data_t>(this->data, index, FWD(fn), FWD(args)...);
return detail::_collapsing_apply<type>(this->data, index, FWD(fn), FWD(args)...);
}

template <typename Fn, typename... Args>
[[nodiscard]] constexpr auto transform(Fn &&fn, Args &&...args) && noexcept(
detail::_is_nothrow_rts_applicable<
typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack &&, Args &&...>::type,
Fn &&, copack &&, Args &&...>) ->
detail::_copack_injector<
typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack &&, Args &&...>::type,
Fn &&>,
copack &&, Args &&...>) ->
typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack &&, Args &&...>::type
requires typelist_applicable<Fn, copack &&, Args &&...>
{
using type = detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack &&, Args &&...>::type;
return detail::apply_variadic_union<type, data_t>(::std::move(*this).data, index, FWD(fn), FWD(args)...);
return detail::_collapsing_apply<type>(::std::move(*this).data, index, FWD(fn), FWD(args)...);
}

template <typename Fn, typename... Args>
[[nodiscard]] constexpr auto transform(Fn &&fn, Args &&...args) const && noexcept(
detail::_is_nothrow_rts_applicable<typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&,
copack const &&, Args &&...>::type,
Fn &&, copack const &&, Args &&...>) ->
detail::_is_nothrow_rts_applicable<
typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack const &&,
Args &&...>::type,
detail::_copack_injector<typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&,
copack const &&, Args &&...>::type,
Fn &&>,
copack const &&, Args &&...>) ->
typename detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack const &&, Args &&...>::type
requires typelist_applicable<Fn, copack const &&, Args &&...>
{
using type = detail::_copack_apply_result<detail::_collapsing_copack_tag, Fn &&, copack const &&, Args &&...>::type;
return detail::apply_variadic_union<type, data_t>(::std::move(*this).data, index, FWD(fn), FWD(args)...);
return detail::_collapsing_apply<type>(::std::move(*this).data, index, FWD(fn), FWD(args)...);
}
};

Expand Down Expand Up @@ -1284,9 +1348,6 @@ template <::std::size_t I, some_copack C>
}

namespace detail {
// A copack represents void success with pack<>.
template <typename V> using _sum_element_t = ::std::conditional_t<::std::is_void_v<V>, ::fn::pack<>, V>;

// Compare exact branch result types: stripping cv/ref could select a convergent path
// whose type assertion fails. Tpl avoids a dependency on the expected header.
template <template <typename...> typename Tpl, typename E> struct _joining_expected_tag final {};
Expand Down
21 changes: 13 additions & 8 deletions include/fn/detail/functional.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,24 @@
namespace fn::inline LIBFN_VERSION::detail {

namespace _fold_detail {
// pack forbids rvalue references.
template <typename T>
using _element_t = ::std::conditional_t<::std::is_rvalue_reference_v<T>, ::std::remove_reference_t<T>, T>;

// The branch the fold takes is chosen by `if constexpr`, so a single expression cannot state its
// specification: the two untaken spellings would be ill-formed. Hence one specialization per branch.
template <typename L, typename R, typename Lv, typename Rv>
struct _nothrow_fold : ::std::bool_constant<noexcept(::fn::pack<L, R>{::std::declval<Lv>(), ::std::declval<Rv>()})> {};
struct _nothrow_fold : ::std::bool_constant<noexcept(
::fn::pack<_element_t<L>, _element_t<R>>{::std::declval<Lv>(), ::std::declval<Rv>()})> {};
template <typename L, typename R, typename Lv, typename Rv>
requires _some_pack<L>
struct _nothrow_fold<L, R, Lv, Rv>
: ::std::bool_constant<noexcept(::std::declval<Lv>().append(::std::in_place_type_t<R>{}, ::std::declval<Rv>()))> {};
template <typename L, typename R, typename Lv, typename Rv>
requires(not _some_pack<L>) && _some_pack<R>
struct _nothrow_fold<L, R, Lv, Rv> : ::std::bool_constant<noexcept(::fn::pack<L>{::std::declval<Lv>()}.append(
::std::in_place_type_t<R>{}, ::std::declval<Rv>()))> {};
struct _nothrow_fold<L, R, Lv, Rv>
: ::std::bool_constant<noexcept(::fn::pack<_element_t<L>>{::std::declval<Lv>()}.append(::std::in_place_type_t<R>{},
::std::declval<Rv>()))> {};

template <typename L, typename R>
[[nodiscard]] constexpr auto _fold(auto &&l, auto &&r) //
Expand All @@ -42,9 +48,9 @@ template <typename L, typename R>
return FWD(l).append(::std::in_place_type_t<R>{}, FWD(r));
} else {
if constexpr (_some_pack<R>) {
return ::fn::pack<L>{FWD(l)}.append(::std::in_place_type_t<R>{}, FWD(r));
return ::fn::pack<_element_t<L>>{FWD(l)}.append(::std::in_place_type_t<R>{}, FWD(r));
} else {
return ::fn::pack<L, R>{FWD(l), FWD(r)};
return ::fn::pack<_element_t<L>, _element_t<R>>{FWD(l), FWD(r)};
}
}
}
Expand Down Expand Up @@ -162,9 +168,8 @@ template <typename Fn, typename Arg, typename Arg0, typename... Args>
apply<Fn, decltype(::fn::detail::_fold_detail::fold<Arg, Arg0>(FWD(arg), FWD(arg0))), Args...>(
FWD(fn), ::fn::detail::_fold_detail::fold<Arg, Arg0>(FWD(arg), FWD(arg0)), FWD(args)...);
}
// Deduced return: a trailing return type is substituted before constraints are checked, so an
// explicit one would instantiate `fold` for non-viable candidates and static_assert (a `pack` of
// rvalue refs). The body's `using type` alias is inlined only to dodge MSVC's body-local-alias leak.
// Deduced return delays fold instantiation until constraints are checked.
// Inline the return expression to avoid MSVC's body-local alias leak.
[[nodiscard]] constexpr auto apply(Fn &&fn, Arg &&arg, Arg0 &&arg0, Args &&...args) //
noexcept(noexcept(apply<Fn, decltype(::fn::detail::_fold_detail::fold<Arg, Arg0>(FWD(arg), FWD(arg0))), Args...>(
FWD(fn), ::fn::detail::_fold_detail::fold<Arg, Arg0>(FWD(arg), FWD(arg0)), FWD(args)...))) -> decltype(auto)
Expand Down
Loading
Loading