Module: Plumb::Disjunction
Overview
The runtime shared by Or (left-biased choice) and Union (the
lattice join) — the dual of Conjunction. Both try left and retry
right with the original value on failure, and differ only in how types flow:
- a CHOICE may have CONVERTING branches, so its ends genuinely differ:
`Integer | String.transform(:to_i)` accepts a String but produces an Integer.
- a UNION is a type — every branch returns its value untouched — so it is its
own output type, needing no #value_preserving? recursion and no rebuild.
Instance Attribute Summary collapse
-
#children ⇒ Object
readonly
Returns the value of attribute children.
Class Method Summary collapse
- .build(left, right) ⇒ Union, Or
-
.merge_errors(left, right) ⇒ Object?
Combining the errors of failed ALTERNATIVES is a monoid:
nilis the identity, concatenation the operation.
Instance Method Summary collapse
- #call(result) ⇒ Object
-
#initialize(left, right) ⇒ Object
Identical for both nodes, which differ only in how types flow.
-
#input_type ⇒ Object
(A | B).input_type == A.input_type | B.input_type — shared by both nodes.
-
#with_children(children) ⇒ Object
Rebuild around new branches, RECLASSIFYING by what they are.
Instance Attribute Details
#children ⇒ Object (readonly)
Returns the value of attribute children.
24 25 26 |
# File 'lib/plumb/disjunction.rb', line 24 def children @children end |
Class Method Details
.build(left, right) ⇒ Union, Or
16 17 18 19 20 21 22 |
# File 'lib/plumb/disjunction.rb', line 16 def self.build(left, right) if Plumb::Subtyping.value_preserving?(left) && Plumb::Subtyping.value_preserving?(right) Union.new(left, right) else Or.new(left, right) end end |
.merge_errors(left, right) ⇒ Object?
Combining the errors of failed ALTERNATIVES is a monoid: nil is the identity,
concatenation the operation. ASSOCIATIVITY is the law that matters — (A|B)|C
and A|(B|C) are the same set of alternatives and owe the same errors, so
taking only errors.first from the right (as this once did) drops every
alternative after the first in a right-nested union.
Non-destructive: never appends to the left result's own array, which has already been handed out as an #errors value. Costs one extra Array, and only for three or more alternatives.
ONLY for alternatives — a record's or array's errors are a Hash keyed by field/index, which is meaningful structure this is never applied to.
103 104 105 106 107 108 109 110 |
# File 'lib/plumb/disjunction.rb', line 103 def self.merge_errors(left, right) return right if left.nil? return left if right.nil? merged = left.is_a?(::Array) ? left.dup : [left] right.is_a?(::Array) ? merged.concat(right) : merged.push(right) merged end |
Instance Method Details
#call(result) ⇒ Object
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/plumb/disjunction.rb', line 63 def call(result) # Snapshot the input value: @left may flip the cursor to invalid in place, # so we need the original to retry @right on the same object. original = result.value left_result = @left.call(result) return left_result if left_result.valid? # Capture left's errors before reusing the cursor — if @left mutated # `result` in place, `left_result` IS `result` and the reset below would # wipe them. left_raw = left_result.errors right_result = @right.call(result.reset(original)) return right_result if right_result.valid? # Both branches failed. Combine the two error sets, then reuse right's # already-invalid cursor in place rather than allocating another — a union can # be expensive in composite ORed types. `right_result.errors` is read BEFORE # #invalid! overwrites it. merged = Disjunction.merge_errors(left_raw, right_result.errors) right_result.invalid!(errors: merged) end |
#initialize(left, right) ⇒ Object
Identical for both nodes, which differ only in how types flow.
27 28 29 30 31 32 |
# File 'lib/plumb/disjunction.rb', line 27 def initialize(left, right) @left = Composable.wrap(left) @right = Composable.wrap(right) @children = [@left, @right].freeze freeze end |
#input_type ⇒ Object
(A | B).input_type == A.input_type | B.input_type — shared by both nodes.
A Union cannot shortcut this to self the way it can #output_type, because a
branch may ACCEPT more than it describes: a bare-matcher Constraint reports
input_type Any, so a factored String[/d/] | String[/c/] consumes Any, and a
Union claiming to consume only itself would fail String >> that.
Rebuilt through .build, not the receiver's class: a disjunction may be a
computation, but its projections are types, so
(String->Integer | Integer->String).input_type is a Union and compares equal
to a hand-written String | Integer.
Lazy, or #initialize would recurse building its own io types. Returns self when both children are their own input type, so Subtyping.resolved_input converges on identity without allocating.
49 50 51 52 53 |
# File 'lib/plumb/disjunction.rb', line 49 def input_type l = @left.input_type r = @right.input_type l.equal?(@left) && r.equal?(@right) ? self : Disjunction.build(l, r) end |
#with_children(children) ⇒ Object
Rebuild around new branches, RECLASSIFYING by what they are.
57 |
# File 'lib/plumb/disjunction.rb', line 57 def with_children(children) = Disjunction.build(children[0], children[1]) |