Proposal: make choice<Ts...> an alias template for just<copack<Ts...>>. just legalizes copack payloads through two specializations: one for a non-empty copack payload, carrying the branch-wise machinery found in today's choice; and one for every other payload, carrying today's just machinery. just<copack<>> is declared but never defined: a computation that must select one of zero alternatives is absurd, just like today's incomplete choice<>. Code requiring a unit carrier spells it just<void> (the identity-cluster analogue of fn::expected_unit).
Motivation. Today, the identity carrier's fmap is partial: the just::transform member rejects a copack-returning callback (because of the just.hpp payload mandate), while the pipeline verb repairs that rejection through promotion. In transform.hpp, _promoted_choice / applicable_transform_promote change the carrier family within what is categorically a plain functor map. The alias makes the identity functor genuinely total: a copack-returning map lands on just<copack<A, B>>, which is the choice carrier, with neither a carrier crossing nor a special case. The promotion machinery is deleted outright, and some_identity loses a disjunct.
This is not #321. That issue (closed as a no-go) proposed merging the monad into the payload, erasing the two-level algebra. This proposal preserves both levels—copack as the self-flattening payload and the identity monad on top—and moves only the nominal normalization boundary, from choice-the-class to just-the-class. The interaction table survives unchanged:
| expression |
behaviour |
under the alias |
| copack-in-copack |
flattens (maker splices) |
unchanged |
| copack-in-choice |
flattens inside the boundary |
choice_for<copack<A,B>, C> = just<copack_for<A,B,C>> — the splice happens in copack_for, inside just |
| choice-in-copack |
nests (atom) |
copack_for<A, just<copack<B,C>>> — a just is not a copack; nests |
| choice-in-choice |
nests (the copack between blocks the join) |
choice<choice<X>> = just<copack<just<copack<X>>>> — copack intervenes; nests |
It also reverses the #350 partition mandate on the basis of that mandate's own rationale: "just<copack<Ts...>> would behave exactly as choice<Ts...>". The alias accepts that behavioural identity and turns it from a reason for prohibition into a definitional identity. One meaning retains one spelling; that spelling is now canonical rather than exclusive. choice / choice_for remain the recommended way—but no longer the only way—to name this carrier.
Nested identity spellings become precise. just<choice<A,B>> = just<just<copack<A,B>>>: the identity layers are adjacent, so and_then with the identity Kleisli arrow strips them via the monadic join. By contrast, choice<choice<X>> = just<copack<just<copack<X>>>>: a copack intervenes, making the inner carrier an atom alternative, so the same identity bind instead flattens it through the branch-wise superset splice. One join, two licensed mechanisms. This removes the sole boundary at which join ∘ transform and and_then currently disagree, which is why this issue is sequenced before the fn::join issue.
Consequences — breaking, deliberate, to be documented explicitly in the 0.2.0 release notes:
choice::value() returns the copack payload (today it returns *this, an artifact of inheritance).
- The derived-to-base
choice& → copack& conversions disappear; the nominal boundary becomes airtight. For usability, a small const-query surface is forwarded. The general shape is: has_value(in_place_type<T>), yes; get_ptr, no; mutators, no. The exact set is settled during implementation, favoring usability over complexity.
- Diagnostics spell the canonical
just<copack<...>>; library-authored messages may still say "choice".
just is no longer structurally single-branch: its dispatch granularity is the engine's, uniform per payload. Generic code constrained on some_just must be audited for a single-branch assumption before this lands.
- ABI: covered by the 0.2 inline-namespace bump.
Documentation: TYPE_ALGEBRA.md §3 (the just<copack<...>> note reverses direction), while §10 and §11 simplify. Add a dated CHANGELOG.md entry recording what this obsoletes and why.
Assisted-by: Claude:claude-fable-5
Proposal: make
choice<Ts...>an alias template forjust<copack<Ts...>>.justlegalizes copack payloads through two specializations: one for a non-empty copack payload, carrying the branch-wise machinery found in today'schoice; and one for every other payload, carrying today'sjustmachinery.just<copack<>>is declared but never defined: a computation that must select one of zero alternatives is absurd, just like today's incompletechoice<>. Code requiring a unit carrier spells itjust<void>(the identity-cluster analogue offn::expected_unit).Motivation. Today, the identity carrier's fmap is partial: the
just::transformmember rejects a copack-returning callback (because of thejust.hpppayload mandate), while the pipeline verb repairs that rejection through promotion. Intransform.hpp,_promoted_choice/applicable_transform_promotechange the carrier family within what is categorically a plain functor map. The alias makes the identity functor genuinely total: a copack-returning map lands onjust<copack<A, B>>, which is the choice carrier, with neither a carrier crossing nor a special case. The promotion machinery is deleted outright, andsome_identityloses a disjunct.This is not #321. That issue (closed as a no-go) proposed merging the monad into the payload, erasing the two-level algebra. This proposal preserves both levels—
copackas the self-flattening payload and the identity monad on top—and moves only the nominal normalization boundary, fromchoice-the-class tojust-the-class. The interaction table survives unchanged:choice_for<copack<A,B>, C>=just<copack_for<A,B,C>>— the splice happens incopack_for, insidejustcopack_for<A, just<copack<B,C>>>— ajustis not a copack; nestschoice<choice<X>>=just<copack<just<copack<X>>>>— copack intervenes; nestsIt also reverses the #350 partition mandate on the basis of that mandate's own rationale: "
just<copack<Ts...>>would behave exactly aschoice<Ts...>". The alias accepts that behavioural identity and turns it from a reason for prohibition into a definitional identity. One meaning retains one spelling; that spelling is now canonical rather than exclusive.choice/choice_forremain the recommended way—but no longer the only way—to name this carrier.Nested identity spellings become precise.
just<choice<A,B>>=just<just<copack<A,B>>>: the identity layers are adjacent, soand_thenwith the identity Kleisli arrow strips them via the monadic join. By contrast,choice<choice<X>>=just<copack<just<copack<X>>>>: a copack intervenes, making the inner carrier an atom alternative, so the same identity bind instead flattens it through the branch-wise superset splice. One join, two licensed mechanisms. This removes the sole boundary at whichjoin ∘ transformandand_thencurrently disagree, which is why this issue is sequenced before thefn::joinissue.Consequences — breaking, deliberate, to be documented explicitly in the 0.2.0 release notes:
choice::value()returns the copack payload (today it returns*this, an artifact of inheritance).choice&→copack&conversions disappear; the nominal boundary becomes airtight. For usability, a small const-query surface is forwarded. The general shape is:has_value(in_place_type<T>), yes;get_ptr, no; mutators, no. The exact set is settled during implementation, favoring usability over complexity.just<copack<...>>; library-authored messages may still say "choice".justis no longer structurally single-branch: its dispatch granularity is the engine's, uniform per payload. Generic code constrained onsome_justmust be audited for a single-branch assumption before this lands.Documentation: TYPE_ALGEBRA.md §3 (the
just<copack<...>>note reverses direction), while §10 and §11 simplify. Add a dated CHANGELOG.md entry recording what this obsoletes and why.Assisted-by: Claude:claude-fable-5