Skip to content

Reach the uninhabited value side, split the folds, and compare packs - #383

Merged
Bronek merged 9 commits into
mainfrom
bronek/minor_0.1_cleanup
Jul 25, 2026
Merged

Reach the uninhabited value side, split the folds, and compare packs#383
Bronek merged 9 commits into
mainfrom
bronek/minor_0.1_cleanup

Conversation

@Bronek

@Bronek Bronek commented Jul 25, 2026

Copy link
Copy Markdown
Member

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

verb member pipe, before pipe, after
transform, and_then identity ✅ delegates to the vacuous member
inspect, filter, fail (none) ✅ operand passes through
discard (none)

Two concepts name the operand class — some_empty_value, and its mirror some_empty_error (the identity expected and only it, which some_identity now 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 in concepts.cpp against void, 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_transform and 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 what monadic_invocable answers — the constraint operator| 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/fail cannot 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_or admits a void value side. value_or() substitutes the empty value, so a failed expected<void, E> comes back engaged; any argument makes the call non-viable, there being nothing to build the empty value from. Its sibling recover has always served void expecteds, so the exclusion was an arity accident rather than a decision.
  • expected_unit names expected<void, copack<>>, the family's unit.
  • conjoin and disjoin each take one world. A 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, and a second arm folds operator& over carriers instead; a mixed list matches nothing rather than being resolved by its leading argument. disjoin is carriers-only in every arity, where it previously reached the built-in operator| and folded two integers into 3.
  • pack compares element by element, <=> lexicographically. Defaulting would have deleted the comparisons for a reference element — and a pack of references is what as_pack builds from lvalues — so the folds are written out and compare referents, the semantics of fn::optional<T&> and std::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 <. Neither constexpr nor an exception specification is spelled, both being computed — spelling either is a hard error for an element whose own comparison does not match. A copack over packs becomes comparable as a consequence.

Behaviour changes worth knowing

conjoin(a, b) over two carriers stops producing pack<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

Bronek added 8 commits July 25, 2026 20:21
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
@augmentcode

augmentcode Bot commented Jul 25, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This PR expands libfn’s monadic verb layer to correctly handle carriers whose value side is uninhabited (notably optional<copack<>> and expected<copack<>, E>), aligns pipeline applicability with the already-vacuous member semantics, and adds explicit element-wise comparison for fn::pack.

Changes:

  • Adds some_empty_value / some_empty_error concepts and updates some_identity to use the new naming.
  • Makes value-side verbs (transform, and_then, inspect, filter, fail) accept uninhabited-value carriers by passing through / delegating without instantiating callbacks.
  • Extends value_or to support expected<void, E> (only the zero-argument form is viable) and introduces expected_unit = expected<void, copack<>>.
  • Splits the n-ary folds: conjoin now separates “data fold” vs “carrier fold” and refuses mixed lists; disjoin becomes carriers-only in every arity.
  • Implements fn::pack == and <=> explicitly to compare referents for reference elements and to keep non-comparable elements from hard-erroring.
  • Adds targeted tests covering vacuous-callback behavior, noexcept/constexpr expectations, fold refusal rules, and pack comparisons.

Technical Notes: The PR clarifies the split between per-verb applicable_* (callback applicability) and monadic_invocable (pipeline/verb applicability), and uses constrained helper folds in pack_impl so “asking answers” (non-viable) instead of failing during instantiation.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

return (... && static_cast<bool>(lh._element<Is, Ts>::v == rh._element<Is, Ts>::v));
}

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

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

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
@Bronek

Bronek commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

augment review

@sonarqubecloud

Copy link
Copy Markdown

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review completed. No suggestions at this time.

Comment augment review to trigger a new review at any time.

@Bronek
Bronek merged commit 7e7e47d into main Jul 25, 2026
66 checks passed
Bronek added a commit that referenced this pull request Jul 25, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Minor issues

1 participant