You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Both carrier operators are eager: operator& for conjunction and operator| for disjunction. An overloaded operator is an ordinary function call, so both operand expressions are fully evaluated—in unspecified relative order—before the operator body runs. Every "short-circuit" in & and | is therefore semantic selection over values that have already been constructed.
The current lazy spellings are the Kleisli verbs and_then and or_else. They deliberately compute a different algebra: the verbs join or replace, whereas the operators union the values and take the product of the errors.
Add fn::thunk<F>, a wrapper around a nullary invocable, with CTAD as fn::thunk{f} and an accompanying some_thunk concept. Admit it as the right operand of the carrier-level & and |, giving those operators call-by-name evaluation:
lh & fn::thunk{f} invokes f only if lh is engaged (the product needs its value);
lh | fn::thunk{f} invokes f only if lh has failed (left catch stands);
the invocation occurs at most once, inside the operator body, after lh's state is known; this also removes the unspecified-evaluation-order hazard carried by an eager right operand;
folds remain left-associative, as they are today (a | fn::thunk{f} | fn::thunk{g}): each step produces the carrier that discriminates the next step, so each deferred operand is forced only when the preceding fold requires it.
Lifting the Return Value
The thunk's return value enters the operator through the identity-cluster unit. One uniform rule applies: a carrier is taken as-is, while every other return type is embedded through the unit as just<result>:
a reference T& becomes just<pack<T&>>, as explained under "Resolved Questions."
A bare-data or plain return is a total computation. Under |, it is the lazy catch-all that collapses the result into the cluster; under &, it is a lazy pure contributor.
Sequence this work after the choice re-founding (#414), which makes the rule above a single clause. With choice<Ts...> = just<copack<Ts...>>, the copack case requires no special spelling, and the lift never needs to name a second carrier family.
Governing Invariant
lh op fn::thunk{f} has exactly the type of lh op LIFT(f()).
Result types are computed from std::invoke_result_t<F>; the thunk is never invoked to answer a type question. Values determine engagement. The admission table carries over verbatim: any combination rejected by the eager operator, such as mixed expected and optional, remains rejected under the thunk. noexcept is computed from the invocation, the lift, and the fold.
Refused Positions
The thunk is not admitted at the data level. A left pack or copack is total, so the thunk would be forced unconditionally; laziness without a discriminator is vacuous.
Nor is the thunk admitted as a left operand. The left operand is always forced, and accepting a thunk there would falsely suggest that the eager right operand is protected.
Deliberate Asymmetry
The eager operators continue to reject bare-data operands: lh | pack{…} remains ill-formed, and its eager spelling is lh | just{…}.
The lift exists only inside the thunk, where a body naturally ends with return compute();. Requiring just{…} around every return statement would be boilerplate.
Resolved Questions
Both questions were open in the first version of this issue and were settled during the 0.2 planning close-reads.
() -> void under & is admitted. It lifts to just<void>—the strict unit of &, elided from the value product—so lh & fn::thunk{f} with a void-returning f means exactly: "run this if and only if the left succeeded, and contribute nothing." This is lawful because the Monoidal identity laws constrain the value side of the operation, which a void contributor leaves unchanged. It is not a lazy inspect in disguise: inspect receives the value, while the thunk receives nothing. The apparent collision therefore requires one clarifying sentence in the documentation, not a refusal.
() -> T& lifts to just<pack<T&>>.just<T&> is mandated ill-formed, and the standing doctrine in TYPE_ALGEBRA §15 propagates references inside carriers by wrapping them in a pack. Because append splices, lh & fn::thunk{f} with an f returning T& composes like any other operand. For example, expected<A,E> on the left produces expected<pack<A,T&>,E>. This makes T& the one lift case that inserts a pack: an exception to the uniform rule that must be stated explicitly in the documentation rather than left implicit.
Documentation Obligation
The eager and thunked operators are two lawful points in the same family. McBride & Paterson draw exactly this distinction with the miffy/iffy pair in §5 of Applicative Programming with Effects.
Consequently, a & b ≠ a & fn::thunk{[&]{ return b; }} whenever constructing b has observable effects. The eager form performs both computations unconditionally; the thunked form lets the left-hand outcome decide. The documentation must state this non-equivalence, just as the paper documents iffy.
Naming
"Thunk" is the ALGOL 60 term for exactly this object: a nullary procedure that implements call-by-name argument evaluation (Ingerman, 1961). This is the term's original meaning, not a metaphorical extension. No source-level C++ entity claims the name; the ABI term "adjustor thunk" exists below the source level and has a compatible meaning.
It is a noun naming a type, consistent with just, choice, and pack.
Both carrier operators are eager:
operator&for conjunction andoperator|for disjunction. An overloaded operator is an ordinary function call, so both operand expressions are fully evaluated—in unspecified relative order—before the operator body runs. Every "short-circuit" in&and|is therefore semantic selection over values that have already been constructed.The current lazy spellings are the Kleisli verbs
and_thenandor_else. They deliberately compute a different algebra: the verbs join or replace, whereas the operators union the values and take the product of the errors.Add
fn::thunk<F>, a wrapper around a nullary invocable, with CTAD asfn::thunk{f}and an accompanyingsome_thunkconcept. Admit it as the right operand of the carrier-level&and|, giving those operators call-by-name evaluation:lh & fn::thunk{f}invokesfonly iflhis engaged (the product needs its value);lh | fn::thunk{f}invokesfonly iflhhas failed (left catch stands);lh's state is known; this also removes the unspecified-evaluation-order hazard carried by an eager right operand;a | fn::thunk{f} | fn::thunk{g}): each step produces the carrier that discriminates the next step, so each deferred operand is forced only when the preceding fold requires it.Lifting the Return Value
The thunk's return value enters the operator through the identity-cluster unit. One uniform rule applies: a carrier is taken as-is, while every other return type is embedded through the unit as
just<result>:packbecomesjust<pack<...>>;copackbecomesjust<copack<...>>, which is the choice carrier under Re-foundchoiceas an alias ofjustover acopack#414;Tbecomesjust<T>;voidbecomesjust<void>;T&becomesjust<pack<T&>>, as explained under "Resolved Questions."A bare-data or plain return is a total computation. Under
|, it is the lazy catch-all that collapses the result into the cluster; under&, it is a lazy pure contributor.Sequence this work after the
choicere-founding (#414), which makes the rule above a single clause. Withchoice<Ts...>=just<copack<Ts...>>, the copack case requires no special spelling, and the lift never needs to name a second carrier family.Governing Invariant
lh op fn::thunk{f}has exactly the type oflh op LIFT(f()).Result types are computed from
std::invoke_result_t<F>; the thunk is never invoked to answer a type question. Values determine engagement. The admission table carries over verbatim: any combination rejected by the eager operator, such as mixedexpectedandoptional, remains rejected under the thunk.noexceptis computed from the invocation, the lift, and the fold.Refused Positions
The thunk is not admitted at the data level. A left
packorcopackis total, so the thunk would be forced unconditionally; laziness without a discriminator is vacuous.Nor is the thunk admitted as a left operand. The left operand is always forced, and accepting a thunk there would falsely suggest that the eager right operand is protected.
Deliberate Asymmetry
The eager operators continue to reject bare-data operands:
lh | pack{…}remains ill-formed, and its eager spelling islh | just{…}.The lift exists only inside the thunk, where a body naturally ends with
return compute();. Requiringjust{…}around every return statement would be boilerplate.Resolved Questions
Both questions were open in the first version of this issue and were settled during the 0.2 planning close-reads.
() -> voidunder&is admitted. It lifts tojust<void>—the strict unit of&, elided from the value product—solh & fn::thunk{f}with a void-returningfmeans exactly: "run this if and only if the left succeeded, and contribute nothing." This is lawful because the Monoidal identity laws constrain the value side of the operation, which a void contributor leaves unchanged. It is not a lazyinspectin disguise:inspectreceives the value, while the thunk receives nothing. The apparent collision therefore requires one clarifying sentence in the documentation, not a refusal.() -> T&lifts tojust<pack<T&>>.just<T&>is mandated ill-formed, and the standing doctrine in TYPE_ALGEBRA §15 propagates references inside carriers by wrapping them in apack. Becauseappendsplices,lh & fn::thunk{f}with anfreturningT&composes like any other operand. For example,expected<A,E>on the left producesexpected<pack<A,T&>,E>. This makesT&the one lift case that inserts apack: an exception to the uniform rule that must be stated explicitly in the documentation rather than left implicit.Documentation Obligation
The eager and thunked operators are two lawful points in the same family. McBride & Paterson draw exactly this distinction with the
miffy/iffypair in §5 of Applicative Programming with Effects.Consequently,
a & b≠a & fn::thunk{[&]{ return b; }}whenever constructingbhas observable effects. The eager form performs both computations unconditionally; the thunked form lets the left-hand outcome decide. The documentation must state this non-equivalence, just as the paper documentsiffy.Naming
"Thunk" is the ALGOL 60 term for exactly this object: a nullary procedure that implements call-by-name argument evaluation (Ingerman, 1961). This is the term's original meaning, not a metaphorical extension. No source-level C++ entity claims the name; the ABI term "adjustor thunk" exists below the source level and has a compatible meaning.
It is a noun naming a type, consistent with
just,choice, andpack.Assisted-by: Claude:claude-fable-5