Add the monadic join as both a pipeline verb (x | fn::join()) and an endo member (.join()).
Specification
join is exactly and_then with the identity Kleisli arrow. Semantically, x | fn::join() ≡ x | fn::and_then(λv. v) wherever the latter is well-formed, and is ill-formed otherwise.
The implementation extracts (moves) the inner carrier directly. It deliberately does not use std::identity: its T&& return would rely on auto decay to avoid a dangling reference, creating a hazard for future refactoring. The equivalence is the specification, not the implementation.
All behavior follows from and_then's admission table:
- Member
.join() is a strict, same-kind endomorphism. It flattens expected<expected<T,E>,E>, optional<optional<T>>, just<just<T>>, and a choice of choices through the branch-wise superset splice. A copack-graded nesting unions the grades (the graded μ: G_E ∘ G_F → G_{E∪F}).
- Pipeline
fn::join() supports the endo cases plus the crossings licensed by the and_then verb: just<optional<T>> → optional<T>, expected-at-⊥ evaporation, and the optional bridge.
- Refusals are inherited as well.
expected<expected<T,E1>,E2> with differing ungraded errors is rejected in both forms, leaving the grading opt-in doctrine untouched.
copack has no join in either spelling. This follows automatically: a copack is data rather than a carrier (some_monadic_type excludes it), and its flattening is an invariant rather than an operation. A dedicated diagnostic should state: "a copack is self-flattening; there is nothing to join".
A public applicable_join concept accompanies the verb. It is defined by delegation to the and_then applicability families with the identity arrow, making rejection substitution-visible in accordance with the library-wide constraint discipline.
Law
This addition makes the following law pinnable for carrier-returning callbacks:
and_then(f) ≡ join ∘ transform(f)
This is the defining equation bind = join ∘ fmap, expressed as a compile-time test. The following witness compiles today with g++ 16.2.1 and clang++ 22.1.8 under -std=c++20 -Wall -Wextra -Werror:
fn::just<int> j{42};
auto nested = j | fn::transform([](int) { return fn::choice_for<A, B>{A{}}; });
static_assert(std::same_as<decltype(nested), fn::just<fn::choice_for<A, B>>>); // fmap nests
auto joined = std::move(nested)
| fn::and_then([](auto &&v) { return std::forward<decltype(v)>(v); });
static_assert(std::same_as<decltype(joined), fn::choice_for<A, B>>); // join strips
Sequencing
Implement this after the choice re-founding (#414).
With choice<Ts...> = just<copack<Ts...>>, the equation holds with no carve-outs. transform never changes the carrier family: a copack-returning map lands flat on just<copack<A,B>> (nothing is nested, so join and and_then both refuse), while a choice-returning map lands on the genuinely nested just<just<copack<A,B>>>, which join strips. Without the re-founding, join's documented domain would retain the pipeline-promotion boundary only to delete it.
Presentation
μ remains derived. The library's graded monad is defined by unit plus Kleisli extension (bind), matching the papers' extension form, and join is definitionally and_then(identity): a named convenience, never a new primitive.
This issue also subsumes the parked collapse operation for flattening a nested choice. splice—opening a choice inside a copack at the data level—remains a separate future item.
Assisted-by: Claude:claude-fable-5
Add the monadic join as both a pipeline verb (
x | fn::join()) and an endo member (.join()).Specification
joinis exactlyand_thenwith the identity Kleisli arrow. Semantically,x | fn::join()≡x | fn::and_then(λv. v)wherever the latter is well-formed, and is ill-formed otherwise.The implementation extracts (moves) the inner carrier directly. It deliberately does not use
std::identity: itsT&&return would rely onautodecay to avoid a dangling reference, creating a hazard for future refactoring. The equivalence is the specification, not the implementation.All behavior follows from
and_then's admission table:.join()is a strict, same-kind endomorphism. It flattensexpected<expected<T,E>,E>,optional<optional<T>>,just<just<T>>, and a choice of choices through the branch-wise superset splice. A copack-graded nesting unions the grades (the graded μ: G_E ∘ G_F → G_{E∪F}).fn::join()supports the endo cases plus the crossings licensed by theand_thenverb:just<optional<T>>→optional<T>, expected-at-⊥ evaporation, and the optional bridge.expected<expected<T,E1>,E2>with differing ungraded errors is rejected in both forms, leaving the grading opt-in doctrine untouched.copackhas no join in either spelling. This follows automatically: a copack is data rather than a carrier (some_monadic_typeexcludes it), and its flattening is an invariant rather than an operation. A dedicated diagnostic should state: "a copack is self-flattening; there is nothing to join".A public
applicable_joinconcept accompanies the verb. It is defined by delegation to theand_thenapplicability families with the identity arrow, making rejection substitution-visible in accordance with the library-wide constraint discipline.Law
This addition makes the following law pinnable for carrier-returning callbacks:
and_then(f)≡join ∘ transform(f)This is the defining equation
bind = join ∘ fmap, expressed as a compile-time test. The following witness compiles today with g++ 16.2.1 and clang++ 22.1.8 under-std=c++20 -Wall -Wextra -Werror:Sequencing
Implement this after the
choicere-founding (#414).With
choice<Ts...>=just<copack<Ts...>>, the equation holds with no carve-outs.transformnever changes the carrier family: a copack-returning map lands flat onjust<copack<A,B>>(nothing is nested, sojoinandand_thenboth refuse), while a choice-returning map lands on the genuinely nestedjust<just<copack<A,B>>>, whichjoinstrips. Without the re-founding,join's documented domain would retain the pipeline-promotion boundary only to delete it.Presentation
μ remains derived. The library's graded monad is defined by unit plus Kleisli extension (bind), matching the papers' extension form, and
joinis definitionallyand_then(identity): a named convenience, never a new primitive.This issue also subsumes the parked
collapseoperation for flattening a nested choice.splice—opening a choice inside a copack at the data level—remains a separate future item.Assisted-by: Claude:claude-fable-5