expected<int, Error> & expected<bool, ErrorOther> is ill-formed when neither error type is a copack, while the very same pair disjoins:
using L = fn::expected<int, Error>;
using R = fn::expected<bool, ErrorOther>;
static_assert(requires(L l, R r) { l | r; }); // expected<copack_for<int, bool>, pack<Error, ErrorOther>>
static_assert(requires(L l, R r) { l & r; }); // fails
The conjunction's error channel is the sum: producing the grade is what & does there, so requiring an operand to arrive already graded inverts the rule. Nowhere else in the channel algebra does a sum ask for one — | forms its value sum out of two ungraded value types, in every carrier:
expected<int, E> | expected<bool, E> // expected<copack_for<int, bool>, pack<E, E>>
optional<int> | optional<bool> // optional<copack_for<int, bool>>
choice<int> | choice<bool> // choice_for<int, bool>
Of the four channels — &'s value product and error sum, |'s value sum and error product — this is the only one that was gated.
Four operator& overloads in include/fn/expected.hpp — the different-error arms, one per value shape (void×value, value×void, void×void, value×value) — carry
&& (some_copack<typename ::std::remove_cvref_t<Lh>::error_type>
|| some_copack<typename ::std::remove_cvref_t<Rh>::error_type>)
on top of not is_same_v<El, Er>. Dropping that clause makes each arm serve any distinct pair: every one of them already computes copack_for<El, Er> and lifts each operand's error through the copack<>-guarded _nothrow_arm, so nothing downstream changes. fn::conjoin follows, being the fold of &.
Purely additive — everything newly accepted was ill-formed before, and a matching pair of error types still collapses to the plain error it had.
expected<int, Error> & expected<bool, ErrorOther>is ill-formed when neither error type is acopack, while the very same pair disjoins:The conjunction's error channel is the sum: producing the grade is what
&does there, so requiring an operand to arrive already graded inverts the rule. Nowhere else in the channel algebra does a sum ask for one —|forms its value sum out of two ungraded value types, in every carrier:Of the four channels —
&'s value product and error sum,|'s value sum and error product — this is the only one that was gated.Four
operator&overloads ininclude/fn/expected.hpp— the different-error arms, one per value shape (void×value, value×void, void×void, value×value) — carryon top of
not is_same_v<El, Er>. Dropping that clause makes each arm serve any distinct pair: every one of them already computescopack_for<El, Er>and lifts each operand's error through thecopack<>-guarded_nothrow_arm, so nothing downstream changes.fn::conjoinfollows, being the fold of&.Purely additive — everything newly accepted was ill-formed before, and a matching pair of error types still collapses to the plain error it had.