RFA-253 · Case file with fixtures · Case 225 of 694 · Runtime evidence
Why Option::xor Returns None When Both Values Exist
Option xor means exactly one value must exist. Both Some and both None collapse to None, which loses the difference between conflict and absence; use an explicit Result when diagnostics matter.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Exclusive-or accepts exactly one present operand, making both-present and both-absent collapse to the same None result.
- First discriminating check
- Write the four-case presence table and decide whether the policy is exclusive validation, precedence, or conflict reporting.
Option::xor is not a precedence operator. It represents exclusive presence.
The failing program combines Some("primary") and Some("secondary"). Option::xor returns None, not the first value.
Exclusive means exactly one
The four combinations follow boolean exclusive-or:
None xor None -> None
Some(a) xor None -> Some(a)
None xor Some(b) -> Some(b)
Some(a) xor Some(b) -> None
Both present is false in exclusive-or just like both absent. The method consumes both options and, in the conflict case, drops both inner values.
This can be elegant when None is an acceptable representation of “not exactly one.” It is weak when the caller must know why selection failed.
or expresses precedence instead
If the rule is “prefer the primary, otherwise use secondary,” Option::or expresses that selection. With two present values, it returns the first.
or evaluates its second argument eagerly because it receives an Option value. or_else defers construction when fallback work is expensive or state-changing.
Changing xor to or therefore changes more than one letter: it changes a validation rule into precedence.
None loses two different failures
For command-line flags, authentication methods, configuration sources, or mutually exclusive modes, “neither supplied” and “both supplied” need different messages.
The repaired program matches the pair and returns a Result. It reports missing input separately from conflicting input while preserving the one valid value.
This is slightly longer than xor, but it carries operational evidence. A user can fix “choose one” only if the message says whether to add or remove a choice.
Ownership is consumed even when no value survives
xor takes both options by value. In the both-present case, neither inner value is returned. Their destructors run when the consumed temporaries are dropped.
If values contain handles, guards, or expensive buffers, that lifecycle is real. Using as_ref() can inspect exclusive presence without consuming ownership, followed by an explicit move once selection is validated.
I avoid cloning both values merely to ask which options are present. Presence and ownership transfer can be two steps.
Equality of inner values does not matter
Some(2).xor(Some(2)) still returns None. Exclusive-or examines variants, not equality. Two equal sources are still two supplied sources.
In configuration merging, the product may choose to accept equal duplicate values while rejecting different ones. That requires comparing the inner values explicitly and cannot be expressed by Option::xor alone.
This is a useful example of why structural convenience methods cannot infer domain conflict policy.
Option as boolean is useful but lossy
The standard Option boolean-operator table helps predict and, or, and xor. Some acts like true and None like false, while the inner value is carried when the boolean result has a unique source.
Boolean thinking becomes lossy when errors need identity, priority, or provenance. At that point I move to a match or a domain enum rather than stacking combinators until the policy is invisible.
What I test
The regression covers all four presence combinations. For an explicit Result, it asserts distinct errors and confirms which value is returned. With owned tracked values, it also verifies drop behavior.
At a configuration boundary I add source labels so an error can say which two inputs conflict. An abstract “two values” message may still be insufficient for operators.
More than two sources need a count, not chained xor
Exclusive-or over booleans is associative, but chaining Option::xor over three values answers odd parity, not “exactly one present.” Three Some inputs can leave one Some result even though the configuration has three conflicting sources.
For a list of candidates I count present entries, retain their source names, and accept only count one. This is an important difference between binary exclusive choice and general cardinality validation.
one present -> accept it
zero present -> missing error
two or more -> conflict with source list
The explicit rule scales and gives better diagnostics. A clever fold with xor can pass tests covering only zero, one, and two candidates while failing at three.
References help validate before moving
If candidates are large owned values, I first match left.as_ref() and right.as_ref() or count is_some() results. After the presence rule succeeds, I move only the chosen value out.
This separates checking from destruction. It is also useful when an error response needs to display metadata from both conflicting candidates before they are dropped.
For copyable small values, direct consumption is simpler. I choose based on lifecycle, not a desire to avoid one match expression.
The core principle is that selection and validation are different operations. Option::xor validates exactly-one presence but represents both invalid cases as None. Use it when that loss is acceptable. Use or for precedence, or an explicit Result when absence and conflict must remain distinguishable.