Repository navigation
Refuse bare operands, remove the value slot, deconstruct in the Rust shape and always raise on cross-type equality, releasing 0.10.0 - #82
Closed
nz wants to merge 11 commits into
Conversation
or already refused a bare operand with Errgonomic::ArgumentError. and, xor, zip and zip_with did not: and handed the bare operand back out of the Option chain, xor and zip fell into a NoMethodError on some? or none?, and whether anything fired at all depended on the receiver's variant, so a None-heavy path never learned its operand was wrong. The check now runs before the discriminant is consulted, on all five combinators, with or's message.
Some, Ok and Err exposed value as a public accessor, so a wrapper could be mutated through an alias, a Hash key could move buckets under its own hash, and the reader reached the inner value with no None branch, the ambiguous failure the type exists to prevent. The reader is now protected, for the sibling reads equality, ordering and zip need, and every instance freezes on construction. A call to value or value= from outside reaches method_missing and gets the combinator teaching. sequence_options and sequence_results were the only readers outside the classes; they collect through tap_some and tap_ok instead.
deconstruct answered [self, value] for a Some, an Ok and an Err, and
[None] for a None, so the Rust-shaped pattern `in Some(v)` bound the
wrapper to v under an else and missed without one, nesting such as
`in Ok(Some(v))` could not be written, and None was asymmetric with
Some. It now answers [value] for Some, Ok and Err and [] for None, the
shape Data.define(:value) and Rust's tuple variants share, so a
two-branch case/in with no else is exhaustive and a wrong type reaches
NoMatchingPatternError. There is no deconstruct_keys: a one-payload
sum type has no named field, and a Some around a Hash nests through
the Hash's own protocol as `in Some({id:})`.
Some, None, Ok and Err are also defined as top-level constants beside
the constructors of the same name, so a pattern reads as Rust does.
Rails defines none of the four.
Some(1) == 1 answering false is the silent wrong branch that mirrors a wrapper written into a string, and 0.9.0 had the polarity backwards with the raise opt-in. ==, !=, eql? and === between an Option or a Result and anything that is not one now always raise Errgonomic::TypeMismatchError with the 0.9.0 message, and === is defined so the message names the operator that was written. Errgonomic.strict_equality=, strict_equality? and with_strict_equality are gone, which also removes the process-global flag that leaked across threads. eql? is not exempted. The raise surface is uneven because Ruby's hashing compares hash values first and asks eql? only of a candidate whose hash matches, so Hash#[], Set and uniq stay quiet where Array#include?, Array#-, Array#== and case/when compare pairwise and raise. Strict equality never answers wrong; it only sometimes fails to catch. nil == Some(1) is answered by NilClass and cannot be intercepted. The doctests on == state each of these.
Cross-type equality raises unconditionally now, so the second run of the Rails suite with the flag on is the same run as rake test. The task, its support file, the CI step and the CONTRIBUTING rung go.
lenient_inner_value_comparison? and its writer were read by nothing, and the reader's ||= true could never be turned off anyway.
Err() carries an internal sentinel in place of a value, and a sentinel must never bind to a pattern variable. It now deconstructs to [], so `in Err` and `in Err()` match it and `in Err(e)` matches only an Err that carries a value.
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Errgonomic 0.10.0 closes the four container-level issues the Sprout mass conversion filed against 0.9.0. They are a bare operand escaping
Option#and, a publicvalueslot onSome,OkandErr, adeconstructshape that missedin Some(v), and a cross-type==that answeredfalsebehind an opt-in flag. It stacks on the 0.9.1 branch, so this pull request is opened against2026-09-10-errgonomic-0.9.x-triage/to-s-raiseand merges after it. One choice below is the reviewer's to confirm: the top-level constantsSome,None,OkandErr, which the decision note marked as strikeable.Closes #76, #77, #78, #79.
What changes
Option#and,#xor,#zipand#zip_withraiseErrgonomic::ArgumentErroron a bare operand, on aNonereceiver as well as aSome, with the messageoralready gave. 0.9.x letSome(2).and(3)return3, let aNonereceiver accept anything, and letzipandxorfall into a bareNoMethodError.Some,OkandErrno longer exposevalueorvalue=, and every Option and Result is frozen on construction. A call from outside reachesmethod_missingand gets the combinator teaching asErrgonomic::UnwrappedAccessError.deconstructanswers[value]forSome,OkandErrand[]forNone, soin Some(v),in None,in Ok(Some(v))andin Some({ id: })all match, and a two-branchcase/inwith noelseraisesNoMatchingPatternErroron a wrong type. A value-lessErr()deconstructs to[], soin Errandin Err()match it andin Err(e)matches only anErrthat carries a value. There is nodeconstruct_keys.Some,None,OkandErrare also top-level constants for the four classes.==,!=,eql?and===between a wrapper and anything that is not one always raiseErrgonomic::TypeMismatchErrorwith the 0.9.0 message.Errgonomic.strict_equality=,strict_equality?,with_strict_equalityandrake test:strictare removed.How it works
lib/errgonomic/option.rb:option_operand!runs before the discriminant in the five binary combinators;valueis a protected reader onSomeand the constructor freezes;deconstructisto_a;strict_equality!no longer consults a flag, and===is defined so the message names the operator written. The doctests on==state the raise surface: which collection operations compare pairwise and raise, which go through a hash and stay quiet, and thatnil == Some(1)is answered byNilClass.lib/errgonomic/result.rb: the same shape forOkandErr, with the protected reader onResult::Any.lib/errgonomic.rb: the strict-equality flag and its block form are gone, and so is the unreadlenient_inner_value_comparison?switch.lib/errgonomic/core_ext/enumerable.rb:sequence_optionsandsequence_resultswere the only readers ofvalueoutside the classes; they collect throughtap_someandtap_ok.README.md: the pattern-matching examples take the Rust shape, the value-not-a-slot paragraph says whyvalueis absent, and a Strict equality section states the raise surface.CHANGELOG.md: the 0.10.0 section and its upgrading note.Every reproduction line in the four issues is a doctest on the method it exercises.
Evidence
Run locally on 2026-09-10, on aarch64-darwin inside the flake devshell:
bundle exec rubocop: 23 files inspected, 6 offenses, all six pre-existing onmain(Metrics onmethod_missinginoption.rbandresult.rb, Metrics onoptional_dig, Style/Documentation onErrgonomic::Railsinactive_record_optional.rb). None is in code this branch touches.bundle exec yard doctest: 187 runs, 486 assertions, 0 failures, 0 errors.bundle exec rake test: 132 runs, 464 assertions, 0 failures, 0 errors. This is the Rails integration suite, now under strict equality unconditionally.bundle exec rake: both of the above, same counts.nix build .#errgonomic: builterrgonomic-0.10.0, withrakein its check phase.nix flake check --all-systems: all checks passed.active_record,action_view,active_storage,action_textandrailsand foundSome,None,OkandErrundefined at top level, so the constants collide with nothing Rails defines.CI runs
yard doctestandrake teston Ruby 3.4 on ubuntu-latest, the coverage this machine cannot give.What is left open
.valuenowhere, and the Rails suite and the Rails doctests passed under strict equality with no seam changed, so the two lists the charter asked for are empty. That is evidence from this gem's own suite only; the sprout-validation run againstluis/prod-1613-mass-conversion-probeis where the application-side blast radius is measured. Recommendation: accept, and read the Sprout count before the release is cut.option.rbandresult.rbplus the README examples reverting to the long class names; thedeconstructshape does not depend on them. Recommendation: keep them, sincein Some(v)is the whole point of the shape.Background
Project charter:
cowork/projects/2026-09-10-errgonomic-0.9.x-triage/README.md, with the three decisions underdecisions/dated 2026-09-10.