Reach the uninhabited value side, split the folds, and compare packs - #383
Conversation
Assisted-by: Claude:claude-opus-5
The verbs now reach as far as the members do over an optional<copack<>> or expected<copack<>, E>: the operation is the identity and the callback is never instantiated. The operand class is named some_empty_value, beside its mirror some_empty_error, which some_identity now spells. Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-opus-5
A carrier among the arguments became a pack element, packing the carriers themselves rather than conjoining what they carry - and reaching their values would need value(), which throws. A single argument is still forwarded unchanged, carrier or not. Assisted-by: Claude:claude-opus-5
conjoin gains the mirror of its data fold, shaped like disjoin's: all-carrier arguments fold operator& over the carriers themselves. disjoin now takes carriers in every arity, where it used to reach the built-in operator| and fold two integers into 3. Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-opus-5
Defaulting the comparison would have deleted it for a reference element, and a pack of references is what as_pack builds from lvalues - so the folds live in pack_impl beside the other element-wise machinery, and compare referents as optional<T&> and std::tuple do. Neither constexpr nor an exception specification is spelled: both are computed, and spelling either is a hard error for an element whose own comparison does not match. An element brings its own <=> or the pack has none; no ordering is synthesized from <. Closes #380 Assisted-by: Claude:claude-opus-5
Assisted-by: Claude:claude-opus-5
🤖 Augment PR SummarySummary: This PR expands libfn’s monadic verb layer to correctly handle carriers whose value side is uninhabited (notably Changes:
Technical Notes: The PR clarifies the split between per-verb 🤖 Was this summary useful? React with 👍 or 👎 |
| return (... && static_cast<bool>(lh._element<Is, Ts>::v == rh._element<Is, Ts>::v)); | ||
| } | ||
|
|
||
| template <typename Self> |
There was a problem hiding this comment.
_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
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
common_comparison_category_t answers void - a valid return type, not a substitution failure - when an element's <=> answers something which is not a comparison category, or when the elements' categories have no common one. The ordering was therefore viable to ask about and ill-formed to use; it now says so in its constraint. The comparisons also gain runtime rows which make each fold decide at the first element, walk past it, and run to the end: constant evaluation instantiates those bodies without executing a branch, so the static_assert rows alone left them cold. Assisted-by: Claude:claude-opus-5
|
augment review |
|
…383) * Add the expected_unit type alias Assisted-by: Claude:claude-opus-5 * Admit an uninhabited value side in the value-side verbs The verbs now reach as far as the members do over an optional<copack<>> or expected<copack<>, E>: the operation is the identity and the callback is never instantiated. The operand class is named some_empty_value, beside its mirror some_empty_error, which some_identity now spells. Assisted-by: Claude:claude-opus-5 * Admit a void value side in value_or Assisted-by: Claude:claude-opus-5 * Refuse carriers in the conjoin data fold A carrier among the arguments became a pack element, packing the carriers themselves rather than conjoining what they carry - and reaching their values would need value(), which throws. A single argument is still forwarded unchanged, carrier or not. Assisted-by: Claude:claude-opus-5 * Fold carriers in conjoin and restrict disjoin to them conjoin gains the mirror of its data fold, shaped like disjoin's: all-carrier arguments fold operator& over the carriers themselves. disjoin now takes carriers in every arity, where it used to reach the built-in operator| and fold two integers into 3. Assisted-by: Claude:claude-opus-5 * Compare packs element by element Defaulting the comparison would have deleted it for a reference element, and a pack of references is what as_pack builds from lvalues - so the folds live in pack_impl beside the other element-wise machinery, and compare referents as optional<T&> and std::tuple do. Neither constexpr nor an exception specification is spelled: both are computed, and spelling either is a hard error for an element whose own comparison does not match. An element brings its own <=> or the pack has none; no ordering is synthesized from <. Closes #380 Assisted-by: Claude:claude-opus-5 * Add a CHANGELOG entry for #380 Assisted-by: Claude:claude-opus-5 * Refuse a pack ordering with no common comparison category common_comparison_category_t answers void - a valid return type, not a substitution failure - when an element's <=> answers something which is not a comparison category, or when the elements' categories have no common one. The ordering was therefore viable to ask about and ill-formed to use; it now says so in its constraint. The comparisons also gain runtime rows which make each fold decide at the first element, walk past it, and run to the end: constant evaluation instantiates those bodies without executing a branch, so the static_assert rows alone left them cold. Assisted-by: Claude:claude-opus-5



Closes #380 — the issue's six items, one commit each (plus the merge of #382 and this CHANGELOG entry).
The uninhabited value side
An
optional<copack<>>is never engaged and anexpected<copack<>, E>always holds its error. Their members have been the identity there since the empty-sum doctrine — 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_theninspect,filter,faildiscardTwo concepts name the operand class —
some_empty_value, and its mirrorsome_empty_error(the identityexpectedand only it, whichsome_identitynow spells as its third alternative). Both are well-formed for any type, answering false: the carrier test short-circuits before the value or error type is named. Pinned inconcepts.cppagainstvoid,int[],void(), a member pointer, an incomplete type — each asserted through a negated template concept, so a poisoned atomic constraint would hard-error rather than pass.applicable_transformand friends keep answering false for these operands, deliberately: a vacuous operation consults no callback, so no callback is applicable to it. What applies is the verb, which is whatmonadic_invocableanswers — the constraintoperator|itself carries. Both concepts now document that split; it was previously discoverable only by reading the constraint.The refusal in the identity cluster stands and is not in tension: there
filter/failcannot do their work, a rejection having nowhere to go. Over an uninhabited value side the error channel is live and the operand is in it already, so those verbs are dead code — which the type system proves.The rest
value_oradmits a void value side.value_or()substitutes the empty value, so a failedexpected<void, E>comes back engaged; any argument makes the call non-viable, there being nothing to build the empty value from. Its siblingrecoverhas always served void expecteds, so the exclusion was an arity accident rather than a decision.expected_unitnamesexpected<void, copack<>>, the family's unit.conjoinanddisjoineach take one world. A carrier amongconjoin's arguments used to become apackelement — packing the carriers rather than conjoining what they carry, and reaching their values would needvalue(), which throws. The data fold now refuses carriers, and a second arm foldsoperator&over carriers instead; a mixed list matches nothing rather than being resolved by its leading argument.disjoinis carriers-only in every arity, where it previously reached the built-inoperator|and folded two integers into3.packcompares element by element,<=>lexicographically. Defaulting would have deleted the comparisons for a reference element — and a pack of references is whatas_packbuilds from lvalues — so the folds are written out and compare referents, the semantics offn::optional<T&>andstd::tuple, whose assignment is the pointer-like operation, not their comparison. Every element decides; one that cannot be compared leaves the operator non-viable rather than ill-formed. No ordering is synthesized from<. Neitherconstexprnor an exception specification is spelled, both being computed — spelling either is a hard error for an element whose own comparison does not match. Acopackover packs becomes comparable as a consequence.Behaviour changes worth knowing
conjoin(a, b)over two carriers stops producingpack<carrier, carrier>and starts folding the carrier&;disjoin(1, 2)stops compiling. Both were the point of the issue, and neither had an in-tree caller.Verification
Six-config gate (gcc/clang × Debug/Release/validate) green, C++26 mode green (86/86), MSVC 17 green, dev 43/43.
🤖 Generated with Claude Code
https://claude.ai/code/session_0198ikYYxtCqwNG54SS1b249