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

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.

## The verbs reach an uninhabited value side, and `pack` compares — 25 July 2026

- **Every value-side verb now serves a carrier whose value side is uninhabited** ([#380](https://github.com/libfn/functional/issues/380)): `optional<copack<>>`, never engaged, and `expected<copack<>, E>`, always holding its error. Their members have been the identity there since the empty-sum doctrine (17 July below) — the mapping returns `*this` and the callback is neither invoked nor instantiated — but the verbs answered false, so a pipeline could not reach what the member could. `transform` and `and_then` delegate to those vacuous members; `inspect` observes nothing, `filter` rejects nothing and `fail` never fires, each passing the operand through; `discard` always accepted them. The operand class is named: `some_empty_value` for the value side and `some_empty_error` for its mirror, the identity `expected` and only it, which `some_identity` now spells as its third alternative. Both answer for any type at all — the carrier test short-circuits before the value or error type is named — so their negations stay usable in constraints.
- **Applicability remains a property of the callback**: the `applicable_` concepts keep answering false for these operands, because a vacuous operation consults no callback and so no callback is applicable to it. What applies is the verb, which is the question `monadic_invocable` answers — the constraint `operator|` itself carries, asked of the verb's own `apply` object so that every arm counts. Both concepts now say so in their documentation, the distinction having been discoverable only by reading the constraint.
- **Where the identity cluster refuses, an uninhabited value side accepts, and the difference is not arbitrary**: `filter` and `fail` refuse `just`, `choice` and the identity `expected` (18 July below) because a rejection there has nowhere to go — the operation cannot do its work. Over an uninhabited value side the error channel is live and the operand is in it already, so the same verbs are merely dead code, which the type system proves rather than the caller promising it.
- **`value_or` admits a void value side**: `value_or()` substitutes the empty value, so a failed `expected<void, E>` comes back engaged, and any argument makes the call non-viable, there being nothing to build the empty value from. Previously excluded by an arity accident rather than a decision — its sibling `recover` has always served void expecteds — which also left the family's unit outside a verb that is otherwise total over it. `expected_unit` now names that unit, `expected<void, copack<>>`.
- **`conjoin` and `disjoin` each take one world, and never mix them**: a monadic carrier among `conjoin`'s arguments used to become a `pack` element, packing the carriers rather than conjoining what they carry — and reaching their values would need `value()`, which throws. The data fold now refuses carriers outright, and a second arm folds `operator&` over carriers instead, so `conjoin(a, b)` means the conjunction when both are carriers and the product when neither is; a mixed list matches nothing rather than being resolved by its leading argument. `disjoin` is likewise carriers-only in every arity, where it used to reach the built-in `operator|` and fold two integers into 3.
- **`pack` compares element by element, `<=>` lexicographically**: defaulting the comparisons would have deleted them for a reference element, and a pack of references is what `as_pack` builds from lvalues (24 July below), so the folds are written out and compare referents — the semantics of `fn::optional<T&>` and of `std::tuple`, whose *assignment* is the pointer-like operation, not their comparison. Every element decides, and one that cannot be compared leaves the operator non-viable rather than ill-formed; an element brings its own `<=>` or the pack has no ordering, none being synthesized from `<`. Neither `constexpr` nor an exception specification is spelled, both being computed — and spelling either is a hard error for an element whose own comparison does not match it. A `copack` over packs becomes comparable as a consequence, its equality having always required each alternative to be equality-comparable.

## `expected`'s comparison against a value answers instead of recursing — 25 July 2026

- **That comparison is no longer a hidden friend** ([#381](https://github.com/libfn/functional/issues/381)): alone among the equality operators it constrains itself on the *other* operand — `*x == v` must be valid and convertible to `bool`, an extension over [expected.object.eq]'s Mandates, so that asking answers rather than erroring inside the body. As a hidden friend of the shared storage base its own operand could only be spelled through the policy, a non-deduced context, so deduction rejected nothing and the constraint was evaluated for *every* left operand, whatever its type — constraints are checked before conversion sequences are formed. Where the right operand reached the same operator by ADL — a function pointer returning that `expected`, or any class template over one — the constraint asked the question it was answering, and both compilers reject that outright: `fn::just<E (*)(int)>` could not be compared, nor even asked about. Declared at namespace scope, once per carrier, the operand is deduced and a left operand which is not that `expected` fails deduction before the constraint is reached; libstdc++ pairs the same constraint with the same deduction, for the same reason. The two are a package: the standard's hidden friend is safe precisely because its Mandates never runs during overload resolution. The three sibling operators keep their form — they constrain on the operands' channels, never on the other operand's type, so they cannot refer to themselves.
Expand Down
11 changes: 11 additions & 0 deletions include/fn/and_then.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,17 @@ struct and_then_t::apply final {
return FWD(v).and_then(FWD(fn));
}

// An uninhabited value side leaves no value to bind - delegate to the member, which is the
// identity there and neither invokes nor instantiates the callback, as in transform.
template <some_monadic_type V, typename Fn>
[[nodiscard]] constexpr auto operator()(V &&v, Fn &&fn) const //
noexcept(noexcept(FWD(v).and_then(FWD(fn)))) //
-> same_kind<V &&> auto
requires some_empty_value<V>
{
return FWD(v).and_then(FWD(fn));
}

// The cluster arms: an identity input binds across the carrier kinds, and the bind follows the
// function. Engine-direct - the members are each carrier's own endo-bind, so the cluster cannot
// ride delegation, and the verb layer is the licensed cross-carrier place. Dropping the input's
Expand Down
28 changes: 26 additions & 2 deletions include/fn/concepts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,31 @@ template <class T>
concept convertible_to_choice
= (not ::std::is_void_v<T>) && requires { static_cast<choice<::std::remove_cvref_t<T>>>(::std::declval<T>()); };

/**
* @brief Checks if a carrier's error side is uninhabited - it can never hold an error
*
* The identity `expected`, and only it: `choice` and `just` have no error channel to empty, so they
* answer false here while `some_identity` below accepts all three.
*
* @tparam T Type to check, possibly cv-ref qualified
*/
template <typename T>
concept some_empty_error = some_expected<T> && empty_copack<typename ::std::remove_cvref_t<T>::error_type>;

/**
* @brief Checks if a carrier's value side is uninhabited - it can never hold a value
*
* An `optional<copack<>>`, which is never engaged, or an `expected<copack<>, E>`, which always holds
* its error: every value-side operation over one is the identity, and its callback is neither
* invoked nor instantiated. `optional` belongs here where it cannot belong to the mirror above - its
* empty state is a state that is, not a channel that can never engage.
*
* @tparam T Type to check, possibly cv-ref qualified
*/
template <typename T>
concept some_empty_value
= (some_expected<T> || some_optional<T>) && empty_copack<typename ::std::remove_cvref_t<T>::value_type>;

/**
* @brief Checks if a type is an identity carrier - a monad which never short-circuits
*
Expand All @@ -167,8 +192,7 @@ concept convertible_to_choice
* @tparam T Type to check, possibly cv-ref qualified
*/
template <typename T>
concept some_identity = some_choice<T> || some_just<T>
|| (some_expected<T> && empty_copack<typename ::std::remove_cvref_t<T>::error_type>);
concept some_identity = some_choice<T> || some_just<T> || some_empty_error<T>;

/**
* @brief TODO
Expand Down
34 changes: 34 additions & 0 deletions include/fn/detail/pack_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,40 @@ template <::std::size_t... Is, typename... Ts>
struct pack_impl<::std::index_sequence<Is...>, Ts...> : _element<Is, Ts>... {
static constexpr ::std::size_t size = sizeof...(Is);

// Comparison is element-wise rather than defaulted, because a defaulted comparison over a
// reference member is deleted outright, where a reference element must compare its REFERENT - the
// semantics of optional<T&> and of std::tuple. Asked of the elements as the fold reaches them, so
// an element which cannot be compared leaves the operator non-viable rather than ill-formed; the
// `&&` fold answers true for the empty pack, and the `||` fold stops at the first element that
// orders. No ordering is synthesized from `<`: an element brings its own `<=>` or none.
template <typename Self>
static constexpr auto _equal(Self const &lh, Self const &rh) //
noexcept((... && noexcept(static_cast<bool>(lh._element<Is, Ts>::v == rh._element<Is, Ts>::v)))) -> bool
requires(... && requires {
{ lh._element<Is, Ts>::v == rh._element<Is, Ts>::v } -> ::std::convertible_to<bool>;
})
{
return (... && static_cast<bool>(lh._element<Is, Ts>::v == rh._element<Is, Ts>::v));
}

// The common category is `void` - a valid return type, not a substitution failure - when an
// element's `<=>` answers something that is not a comparison category, or when the elements'
// categories have no common one. Left unsaid, that would make the operator viable to ask about and
// ill-formed to use, so it is said.
template <typename Self>

@augmentcode augmentcode Bot Jul 25, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_compare()’s type can become void (e.g., if an element’s <=> returns a non-category type, or the element result types have no common comparison category), and then pack::operator<=> can look viable to a requires-probe but hard-error when instantiated. That seems at odds with the stated goal that “asking answers rather than erroring”; consider making the “no common category” case fail the constraint instead of failing in the body.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in b66c477

static constexpr auto _compare(Self const &lh, Self const &rh) //
noexcept((... && noexcept(lh._element<Is, Ts>::v <=> rh._element<Is, Ts>::v)))
-> ::std::common_comparison_category_t<decltype(lh._element<Is, Ts>::v <=> rh._element<Is, Ts>::v)...>
requires(not ::std::is_void_v<
::std::common_comparison_category_t<decltype(lh._element<Is, Ts>::v <=> rh._element<Is, Ts>::v)...>>)
{
using type = ::std::common_comparison_category_t<decltype(lh._element<Is, Ts>::v <=> rh._element<Is, Ts>::v)...>;
type result = type::equivalent;
[[maybe_unused]] bool const ordered
= (((result = (lh._element<Is, Ts>::v <=> rh._element<Is, Ts>::v)) != 0) || ...);
return result;
}

template <typename Self, typename Fn, typename... Args>
requires(not(... || (_some_pack<Args> || _some_copack<Args>)))
static constexpr auto _swap_invoke(Self &&self, Fn &&fn, Args &&...args) //
Expand Down
8 changes: 8 additions & 0 deletions include/fn/expected.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -1866,6 +1866,14 @@ template <typename Err> class expected<void, Err> : private detail::_expected_ba
}
};

/**
* @brief The unit of the `expected` family: a carrier over `void` whose error side is uninhabited
*
* It always holds its empty value - `copack<>` offers no alternative to fail with - so it belongs to
* the identity cluster, and `operator&` elides it from a product.
*/
using expected_unit = expected<void, copack<>>;

// The comparison against a value, at namespace scope for the reason given where its siblings are
// declared in pfn: it is the one equality operator constrained on the OTHER operand, and that is
// safe only where this operand is deduced.
Expand Down
12 changes: 12 additions & 0 deletions include/fn/fail.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,18 @@ struct fail_t::apply final {
}
return type{::std::nullopt};
}

// An uninhabited value side never holds a value to fail on, so the failure can never fire: the
// operand passes through, and the callback is neither invoked nor instantiated. Unlike an identity
// carrier, which is refused above, such an operand does have somewhere to fail into - it is
// already there.
template <some_monadic_type V, typename Fn>
[[nodiscard]] constexpr auto operator()(V &&v, Fn &&) const
noexcept(::std::is_nothrow_constructible_v<::std::remove_cvref_t<V>, V>) -> ::std::remove_cvref_t<V>
requires some_empty_value<V> && detail::_relocatable<V>
{
return FWD(v);
}
};

} // namespace LIBFN_VERSION
Expand Down
19 changes: 19 additions & 0 deletions include/fn/filter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,25 @@ struct filter_t::apply final {
}
return FWD(v);
}

// An uninhabited value side never holds a value to test, so nothing can be rejected: the operand
// passes through, and neither callback is invoked or instantiated. Unlike an identity carrier,
// which is refused above, such an operand does have somewhere to fail into - it is already there.
template <some_expected V, typename Pred, typename OnErr>
[[nodiscard]] constexpr auto operator()(V &&v, Pred &&, OnErr &&) const
noexcept(::std::is_nothrow_constructible_v<::std::remove_cvref_t<V>, V>) -> ::std::remove_cvref_t<V>
requires some_empty_value<V> && detail::_relocatable<V>
{
return FWD(v);
}

template <some_optional V, typename Pred>
[[nodiscard]] constexpr auto operator()(V &&v, Pred &&) const
noexcept(::std::is_nothrow_constructible_v<::std::remove_cvref_t<V>, V>) -> ::std::remove_cvref_t<V>
requires some_empty_value<V> && detail::_relocatable<V>
{
return FWD(v);
}
};

} // namespace LIBFN_VERSION
Expand Down
10 changes: 10 additions & 0 deletions include/fn/inspect.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#ifndef INCLUDE_FN_INSPECT
#define INCLUDE_FN_INSPECT

#include <fn/concepts.hpp>
#include <fn/expected.hpp>
#include <fn/functional.hpp>
#include <fn/functor.hpp>
Expand Down Expand Up @@ -163,6 +164,15 @@ struct inspect_t::apply final {
::fn::apply(FWD(fn)); // side-effects only
return FWD(v);
}

// An uninhabited value side never holds a value, so there is nothing to observe: the operand
// passes through, and the callback is neither invoked nor instantiated.
template <some_monadic_type V, typename Fn>
[[nodiscard]] constexpr auto operator()(V &&v, Fn &&) const noexcept -> V &&
requires some_empty_value<V>
{
return FWD(v);
}
};

} // namespace LIBFN_VERSION
Expand Down
20 changes: 14 additions & 6 deletions include/fn/monadic.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,27 @@ namespace fn {
inline namespace LIBFN_VERSION {

/**
* @brief TODO
* @brief Checks if a type is one of the library's carriers - what a pipeline flows through
*
* @tparam T TODO
* The four kinds a verb may be applied to: `expected`, `optional`, `choice` and `just`. A `pack` or
* a `copack` is data a carrier holds, not a carrier, and answers false.
*
* @tparam T Type to check, possibly cv-ref qualified
*/
template <typename T>
concept some_monadic_type = detail::_some_monadic_type<T>;

/**
* @brief TODO
* @brief Checks if a verb applies to a carrier - the constraint `operator|` itself carries
*
* The question asked of the verb's own `apply` object, so every arm it offers counts. This is what
* a pipeline asks, and it differs from the `applicable_` concept of an individual verb, which asks
* whether the callback is used to serve the operand: an operation over an uninhabited side consults
* no callback at all, and so applies while no callback is applicable to it.
*
* @tparam Functor TODO
* @tparam V TODO
* @tparam Args TODO
* @tparam Functor The verb, such as `fn::transform_t`
* @tparam V The carrier, possibly cv-ref qualified
* @tparam Args The verb's arguments as its functor holds them, typically the callback
*/
template <typename Functor, typename V, typename... Args>
concept monadic_invocable = detail::_monadic_invocable<Functor, V, Args...>;
Expand Down
Loading