Class: Plumb::And

Inherits:
Object
  • Object
show all
Includes:
Composable, Conjunction
Defined in:
lib/plumb/and.rb

Overview

SEQUENTIAL COMPOSITION — a morphism left.source -> right.target, built by Composable#>> when some side changes the value. The pure-refinement case (neither side converts) is Intersection instead; see Conjunction for why the two must be different nodes.

#input_type / #output_type are what the chain as a whole CONSUMES and PRODUCES, not its two sides (those are children): (String -> Integer) >> (Integer -> Integer) consumes String and produces Integer. One hop at construction suffices, since the children are already resolved by the same rule — O(1) per node, and off the memoization path entirely.

Instance Attribute Summary

Attributes included from Conjunction

#children, #input_type, #output_type

Instance Method Summary collapse

Methods included from Conjunction

build, #call, #with_children

Methods included from Composable

#&, #/, #>>, #[], #accepted_type, #as_node, #build, #check, #children, #defer, #fusable_step?, #generate, #idempotent?, included, #input_type, #invalid, #invoke, #match, #metadata, #not, #output_type, #pipeline, #policy, resolve_operand, #static, #to_json_schema, #to_mermaid, #to_plumb_type, #to_s, #transform, #value, #value_preserving?, #where, #with, wrap, #|

Methods included from Callable

#call, #parse, #resolve

Constructor Details

#initialize(left, right) ⇒ And

Returns a new instance of And.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/plumb/and.rb', line 21

def initialize(left, right)
  @left = left
  @right = right
  @input_type = left.input_type
  # A value-preserving right NARROWS what the left produces rather than replacing
  # it, so the chain produces the MEET of the two: `(String -> Integer)` then
  # `where(size: 10)` produces an Integer of size 10, not the bare `(size === 10)`.
  # A converting right replaces the value, so its own output stands.
  #
  # Conjunction.build, not Intersection.new — `left.output_type` need not preserve
  # values (a record drops undeclared keys, a Static replaces it), and only the
  # classifier may decide. The `lo.equal?(left)` guard is the fixpoint: a left that
  # IS its own output type would otherwise recurse forever.
  @output_type = if Plumb::Subtyping.value_preserving?(right)
                   lo = left.output_type
                   lo.equal?(left) ? self : Conjunction.build(lo, right)
                 else
                   right.output_type
                 end
  @children = [left, right].freeze
  freeze
end

Instance Method Details

#absorb_input(type) ⇒ Object



78
79
80
81
# File 'lib/plumb/and.rb', line 78

def absorb_input(type)
  absorbed = @left.absorb_input(type)
  absorbed && Conjunction.build(absorbed, @right)
end

#absorb_output(type) ⇒ Object

Boundary absorption re-associates for the same reason #fuse_with does: the boundary a chain presents to a neighbour belongs to the step at that END of it, so And(gate, f) >> Types::Float reaches f's output slot, and `Types::Integer

And(f, x)reachesf`'s input slot. Nil when that end declines, and the rebuilt half carries its own soundness proof. @see Composable#absorb_output



73
74
75
76
# File 'lib/plumb/and.rb', line 73

def absorb_output(type)
  absorbed = @right.absorb_output(type)
  absorbed && Conjunction.build(@left, absorbed)
end

#fuse_with(other) ⇒ Object

Fuse self >> other by RE-ASSOCIATING: >> is associative, so when the tail can absorb other we rebuild around the fused tail. Without this, one non-fusable step at the head blocks every later step from reducing — a pipeline behind a type gate is And(gate, step1), and only Function-to-Function fuses, so step2 onwards could never join.

The soundness proof stays with the node that owns it: this only re-associates, and @right.fuse_with(other) carries its own boundary check. Nil when the tail declines. Recursion is bounded by the chain's depth.



63
64
65
66
# File 'lib/plumb/and.rb', line 63

def fuse_with(other)
  fused = @right.fuse_with(other)
  fused && Conjunction.build(@left, fused)
end

#subtype_identityObject

Identified for subtyping by what it PRODUCES, like Function and Implementation: the input constraints do not carry through. This is what stops the meet rule reaching a chain that converts. @see Composable#subtype_identity



47
# File 'lib/plumb/and.rb', line 47

def subtype_identity = @output_type