Module: Plumb::Composable
- Includes:
- Callable
- Included in:
- And, AnyClass, ArrayClass, AttributeValueMatch, Codec, Node, Constraint, Deferred, Function, HashClass, HashMap, InterfaceClass, Intersection, Metadata, NeverClass, Not, Or, Pipeline, Pipeline::AroundStep, Policy, RangeClass, StaticClass, StreamClass, TaggedHash, TupleClass, Types::Data, Union, ValueClass
- Defined in:
- lib/plumb/composable.rb
Overview
Composable mixes in composition methods to classes.
such as #>>, #|, #not, and others.
Any Composable class can participate in Plumb compositions.
A host object only needs to implement the Step interface call(Result) => Result
Defined Under Namespace
Classes: Node
Class Method Summary collapse
-
.included(base) ⇒ Object
This only runs when including Composable, not extending classes with it.
-
.resolve_operand(other, op:, left:) ⇒ Composable
Resolve
otheragainst a composition context and wrap it as a Composable — the full normalization for the RIGHT operand of a composition operator. -
.wrap(callable) ⇒ Composable
Wrap an object in a Composable instance.
Instance Method Summary collapse
-
#&(other) ⇒ Composable
Intersection ("and"/meet) — the symmetric dual of #|.
-
#/(other) ⇒ Composable
Compose like #>> but WITHOUT the strict subtype check — the escape hatch for chains the checker can't prove safe but you know are (eg. a narrowing like
Types::Integer / Types::Integer[1..10], or feeding a producer whose output you know the right side accepts). -
#>>(other) ⇒ And
Chain two composable objects together.
-
#[](*args) ⇒ Object
Sugar over #match: a splatted list of values becomes a Set membership matcher, so
Integer[1, 2, 3]==Integer[Set[1, 2, 3]](and composes / reduces like any Set constraint). - #absorb_input(_type) ⇒ Object
-
#absorb_output(_type) ⇒ Object
BOUNDARY ABSORPTION — the one-sided companions to #fuse_with, for a seam where only one side is a typed step and the other is a plain type.
-
#accepted_type ⇒ Object
The type this step accepts as the consumer of a
left >> selfchain — the values its #call processes without rejecting outright. -
#as_node(node_name, args = BLANK_HASH) ⇒ Node
Wrap a Step in a node with a custom #node_name which is expected by visitors.
-
#build(cns, factory_method = :new, &block) ⇒ And
Compose a step that instantiates a class.
-
#check(errors = 'did not pass the check', &block) ⇒ Constraint
Pass the value through an arbitrary validation.
-
#children ⇒ Array<Composable>
Visitors expect a #node_name and #children interface.
-
#defer(definition = nil, &block) ⇒ Object
A helper to wrap a block in a Step that will defer execution.
-
#fusable_step? ⇒ Boolean
Can Function#fuse_with drop this node's boundary checks? Only the node knows: the answer is yes exactly when its #call runs the standard input -> fn -> output mapping, so the checks removed at a seam are checks it would really have run.
-
#fuse_with(_other) ⇒ Object
Fuse
self >> otherinto a single node when the runtime checks at the boundary between them are provably redundant, or nil when fusion doesn't apply. -
#generate(generator = nil, &block) ⇒ And
Return the output of a block or #call interface, regardless of input.
-
#idempotent? ⇒ Boolean
Whether running this step twice in a row is the same as running it once, i.e.
- #input_type ⇒ Object
-
#invalid(errors: nil) ⇒ Not
Like #not, but with a custom error message.
-
#invoke(*args, &block) ⇒ Composable
Build a step that will invoke one or more methods on the value.
-
#match(*args) ⇒ Constraint
Alias of
#[]Match a value using#===. -
#metadata(data = Undefined) ⇒ Hash, And
Return a new Step with added metadata, or build step metadata if no argument is provided.
-
#not(other = self) ⇒ Not
Negate the result of a step.
- #output_type ⇒ Object
-
#pipeline(&block) ⇒ Pipeline
Build a Plumb::Pipeline with this object as the starting step.
-
#policy(*args, &blk) ⇒ Composable
Register a policy for this step.
-
#static(value) ⇒ And
Always return a static value, regardless of the input.
-
#subtype_identity ⇒ Object
The type that carries this node's identity for the subtype relation.
- #to_json_schema(root: false) ⇒ Hash
-
#to_mermaid(direction: 'LR') ⇒ String
Render this composition as a Mermaid
flowchart. -
#to_plumb_type(op:, left:) ⇒ Composable
Composition-context resolution hook, consulted on the RIGHT operand of #>>, #| and #& before wrapping/type-checking — via Composable.resolve_operand, which any custom operator implementation should route its operand through.
- #to_s ⇒ Object
-
#transform(target_type, callable = nil, &block) ⇒ Function
Transform value.
-
#value(val) ⇒ Object
Match a value using
#==Normally you'll build matchers via ``#[], which uses#===`. -
#value_preserving? ⇒ Boolean
Whether this type returns its input value UNCHANGED on success — a coreflexive refinement (a pure filter).
-
#where(attrs) ⇒ Object
Check attributes of an object against values, using #===.
-
#with ⇒ Object
deprecated
Deprecated.
User #where instead
-
#|(other) ⇒ Composable
Chain two composable objects together as a disjunction ("or").
Methods included from Callable
Class Method Details
.included(base) ⇒ Object
This only runs when including Composable, not extending classes with it.
211 212 213 214 |
# File 'lib/plumb/composable.rb', line 211 def self.included(base) base.send(:include, Naming) base.send(:include, Equality) end |
.resolve_operand(other, op:, left:) ⇒ Composable
Resolve other against a composition context and wrap it as a
Composable — the full normalization for the RIGHT operand of a
composition operator. Consults the operand's #to_plumb_type hook when it
has one (raw values — procs, hash literals — don't, and skip straight to
wrapping), so callers never need a #respond_to? check. EVERY custom
operator implementation should route its operand through here.
274 275 276 277 |
# File 'lib/plumb/composable.rb', line 274 def self.resolve_operand(other, op:, left:) other = other.to_plumb_type(op:, left:) if other.respond_to?(:to_plumb_type) wrap(other) end |
.wrap(callable) ⇒ Composable
Wrap an object in a Composable instance.
Anything that includes Composable is a noop.
A Hash is assumed to be a HashClass schema.
An Array with zero or 1 element is assumed to be an ArrayClass.
Any #call(Result) => Result interface is wrapped in a Step.
Anything else is assumed to be something you want to match against via #===.
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/plumb/composable.rb', line 230 def self.wrap(callable) if callable.is_a?(Composable) callable elsif callable.respond_to?(:to_composable) # The context-free resolution hook for objects that become a type on # demand: an Encoder class resolves to its default (declared) # direction, a Codec class to its instance. Used in schema literals, # `Array[Enc]`, `#/`, etc. The composition operators (#>>, #|, #&) # consult #to_plumb_type BEFORE wrapping, so a composed operand can # resolve against context and never reaches this branch. wrap(callable.to_composable) elsif callable.is_a?(::Hash) HashClass.new(schema: callable) elsif callable.is_a?(::Array) element_type = case callable.size when 0 Types::Any when 1 callable.first else raise ArgumentError, '[element_type] syntax allows a single element type' end Types::Array[element_type] elsif callable.respond_to?(:call) Function.opaque(callable) elsif Constraint.literal_matcher?(callable) # Equality literals use ValueClass; broader matchers use Constraint. ValueClass.new(callable) else Constraint.new(callable) end end |
Instance Method Details
#&(other) ⇒ Composable
Intersection ("and"/meet) — the symmetric dual of #|. Builds the greatest
lower bound of self and other: the type describing values that satisfy
BOTH. Unlike #>>, it is order-independent and not subtype-checked. The
reducer (Subtyping.intersect) narrows where it can — intersecting Ranges/
Sets (Integer[2..] & Integer[0..100] == Integer[2..100]), covariant
containers, and distributing over unions — and collapses a PROVABLY-empty
intersection to Types::Never (Integer[2..10] & Integer[11..100],
String & Integer). When it can prove neither a narrowing nor emptiness it
falls back to Conjunction.build — a runtime intersection where both sides
must pass.
462 463 464 465 |
# File 'lib/plumb/composable.rb', line 462 def &(other) other = Composable.resolve_operand(other, op: :&, left: self) Plumb::Subtyping.intersect(self, other) || Conjunction.build(self, other) end |
#/(other) ⇒ Composable
Compose like #>> but WITHOUT the strict subtype check — the escape hatch
for chains the checker can't prove safe but you know are (eg. a narrowing
like Types::Integer / Types::Integer[1..10], or feeding a producer whose
output you know the right side accepts). You assert the composition is
valid; it is still runtime-checked when data flows through. Reduces and
builds the same refinement as #>> (just skipping the build-time check), so
the result participates in subtyping like any other refinement. The / reads as
Pathname#/ does — "join the next segment". When self is the Any top the
right side stands alone, consistent with #[].
430 431 432 |
# File 'lib/plumb/composable.rb', line 430 def /(other) constrain(Composable.wrap(other)) end |
#>>(other) ⇒ And
Chain two composable objects together.
A.K.A "and" or "sequence"
Type-checks the composition by subsumption: everything self produces
must be acceptable to other (self's output a subtype of other's
input), else it raises Plumb::TypeError — eg. String >> Integer, or
Integer[0..40] >> Integer[2..10] (the left can emit values the right
rejects). To narrow a value, use #[] / #transform(...)[...] (a
refinement is a runtime-checked cast, built directly and not subtype-
checked). The check is permissive only where types are unknown: opaque
steps (plain procs, transforms, narrowing matchers) report Any and opt out.
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 |
# File 'lib/plumb/composable.rb', line 400 def >>(other) other = Composable.resolve_operand(other, op: :>>, left: self) # `X >> X` is redundant for a value-preserving validator (eg. Types::String): # validating the same value twice is the same as once. Gated on #idempotent? # so transforms — where `X >> X` would apply the change twice — never collapse. return self if idempotent? && self == other Plumb::Subtyping.check_composable!(self, other) # Drop what `other` re-asserts that `self` already guarantees. reduce_step # folds a base-type gate into a Constraint chain (`Integer[0..100] >> # Integer[-10..110]` -> `Integer[0..100]`, `::Integer` validated once); # redundant_refinement? does the same for any value-preserving `other` that # `self` already subsumes (`String.where(size: 3..10) >> .where(size: 0..)` # -> the former). A non-redundant `other` (a transform, or a narrowing # refinement) stays an And. Plumb::Optimizer.rewrite_step(self, other) end |
#[](*args) ⇒ Object
Sugar over #match: a splatted list of values becomes a Set membership
matcher, so Integer[1, 2, 3] == Integer[Set[1, 2, 3]] (and composes /
reduces like any Set constraint). A single argument is used as-is — a Range,
Regexp, Set, class or literal value.
605 |
# File 'lib/plumb/composable.rb', line 605 def [](*args) = match(args.size > 1 ? ::Set.new(args) : args.first) |
#absorb_input(_type) ⇒ Object
381 |
# File 'lib/plumb/composable.rb', line 381 def absorb_input(_type) = nil |
#absorb_output(_type) ⇒ Object
BOUNDARY ABSORPTION — the one-sided companions to #fuse_with, for a seam
where only one side is a typed step and the other is a plain type. A typed
step already RUNS its boundary types as steps (result.map(input).map(fn) .map(output)), so a neighbouring type can move into the matching slot and
the node then does the same work in one hop instead of two.
`self >> type` -> #absorb_output(type), asked of the LEFT (the step)
`type >> self` -> #absorb_input(type), asked of the RIGHT (the step)
Return the rebuilt node, or nil to decline. Implemented by Function (which owns the soundness conditions) and re-associated by And, so a step in the middle of a chain is reachable. Reached from Optimizer.reduce_step as its LAST rung: absorption only fires where no other reduction applies.
380 |
# File 'lib/plumb/composable.rb', line 380 def absorb_output(_type) = nil |
#accepted_type ⇒ Object
The type this step accepts as the consumer of a left >> self chain — the
values its #call processes without rejecting outright. Defaults to what it
takes as input (its resolved #input_type): right for plain matchers and for
conversion/consumer types (Function, Stream, Pipeline — they accept their
declared input, so they need no override). Two kinds of type override it:
- a refinement (And): its #input_type is only the type the chain opens
with and would drop the constraint it adds, so it accepts its *output*;
- a Hash: it relaxes each field to what that field accepts.
Consulted by Plumb::Subtyping when checking #>>.
331 |
# File 'lib/plumb/composable.rb', line 331 def accepted_type = Plumb::Subtyping.resolved_input(self) |
#as_node(node_name, args = BLANK_HASH) ⇒ Node
Wrap a Step in a node with a custom #node_name which is expected by visitors. So that we can define special visitors for certain compositions. Ex. Types::Boolean is a compoition of Types::True | Types::False, but we want to treat it as a single node.
683 684 685 |
# File 'lib/plumb/composable.rb', line 683 def as_node(node_name, args = BLANK_HASH) Node.new(node_name, self, args) end |
#build(cns, factory_method = :new, &block) ⇒ And
756 757 758 759 760 761 |
# File 'lib/plumb/composable.rb', line 756 def build(cns, factory_method = :new, &block) # Without a block the callable is a fresh lambda per call, so name what the # step actually IS — the (class, factory method) pair — as its identity. transform_step(cns, block || ->(value) { cns.send(factory_method, value) }, identity: block || [cns, factory_method]) end |
#check(errors = 'did not pass the check', &block) ⇒ Constraint
Pass the value through an arbitrary validation
521 522 523 524 525 |
# File 'lib/plumb/composable.rb', line 521 def check(errors = 'did not pass the check', &block) # A refinement, not a sequence: build the matcher over `self` as its base so # the checked value keeps `self`'s type (`User.check { … }` is still a User). Constraint.new(block, base: self, error: errors, label: errors) end |
#children ⇒ Array<Composable>
Visitors expect a #node_name and #children interface.
741 |
# File 'lib/plumb/composable.rb', line 741 def children = BLANK_ARRAY |
#defer(definition = nil, &block) ⇒ Object
A helper to wrap a block in a Step that will defer execution. This so that types can be used recursively in compositions.
286 287 288 |
# File 'lib/plumb/composable.rb', line 286 def defer(definition = nil, &block) Deferred.new(definition || block) end |
#fusable_step? ⇒ Boolean
Can Function#fuse_with drop this node's boundary checks? Only the node knows: the answer is yes exactly when its #call runs the standard input -> fn -> output mapping, so the checks removed at a seam are checks it would really have run. Default false — a node opts in by saying so.
365 |
# File 'lib/plumb/composable.rb', line 365 def fusable_step? = false |
#fuse_with(_other) ⇒ Object
Fuse self >> other into a single node when the runtime checks at the
boundary between them are provably redundant, or nil when fusion doesn't
apply. A reduction rung in Optimizer.reduce_step, so both #>> and #/
reach it. Implemented by Function (transform fusion) and CovariantFusion
(the functor law for containers).
358 |
# File 'lib/plumb/composable.rb', line 358 def fuse_with(_other) = nil |
#generate(generator = nil, &block) ⇒ And
Return the output of a block or #call interface, regardless of input. The block will be called to get the value, on every invocation.
819 820 821 822 823 824 825 826 |
# File 'lib/plumb/composable.rb', line 819 def generate(generator = nil, &block) generator ||= block raise ArgumentError, 'expected a generator' unless generator.respond_to?(:call) Function.opaque(inspect: 'generator', identity: [:generate, generator]) do |r| r.valid(generator.call) end >> self end |
#idempotent? ⇒ Boolean
Whether running this step twice in a row is the same as running it once,
i.e. it validates without changing the value. Lets #>> drop a redundant
X >> X. Default false; only types that never transform the value opt in
(see Constraint). A transform must NOT be idempotent — X >> X would
apply it twice.
338 |
# File 'lib/plumb/composable.rb', line 338 def idempotent? = false |
#input_type ⇒ Object
290 |
# File 'lib/plumb/composable.rb', line 290 def input_type = self |
#invalid(errors: nil) ⇒ Not
Like #not, but with a custom error message.
558 559 560 |
# File 'lib/plumb/composable.rb', line 558 def invalid(errors: nil) Not.new(self, errors:) end |
#invoke(*args, &block) ⇒ Composable
Build a step that will invoke one or more methods on the value. Ex 1: Types::String.invoke(:downcase) Ex 2: Types::Array.invoke(:[], 1) Ex 3 chain of methods: Types::String.invoke([:downcase, :to_sym])
864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 |
# File 'lib/plumb/composable.rb', line 864 def invoke(*args, &block) case args in [::Symbol => method_name, *rest] label = [method_name.inspect, rest.inspect].join(' ') # The block is new on each call, but the step it implements is determined # by the method and its arguments — so name that as the identity, and two # `invoke(:downcase)` steps stay #== (see Function#==). self >> Function.opaque(inspect: label, identity: [:invoke, method_name, rest, block]) do |result| result.valid(result.value.public_send(method_name, *rest, &block)) end in [Array => methods] if methods.all? { |m| m.is_a?(Symbol) } methods.reduce(self) { |step, method| step.invoke(method) } else raise ArgumentError, "expected a symbol or array of symbols, got #{args.inspect}" end end |
#match(*args) ⇒ Constraint
Alias of #[]
Match a value using #===
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 |
# File 'lib/plumb/composable.rb', line 583 def match(*args) # A refinement returns a base-carrying Constraint directly (not an # `And(self, matcher)`): the matcher records `self` as its base, so it # subtypes and composes as "a `self` narrowed by the matcher". When `self` # is the Any top the matcher stands alone (`Any[String]` == the String # matcher), matching the collapsing `AnyClass#>>` provides. Routed through # Constraint.narrow so stacked Range refinements intersect # (`Integer[0..100][10..]` == `Integer[10..100]`). base = is_a?(AnyClass) ? nil : self # A bare equality literal uses ValueClass so `Any[5]` and `Value[5]` share # one subtype identity. A based literal remains a Constraint. return ValueClass.new(args.first) if base.nil? && args.size == 1 && Constraint.literal_matcher?(args.first) Constraint.narrow(base, *args) end |
#metadata(data = Undefined) ⇒ Hash, And
Return a new Step with added metadata, or build step metadata if no argument is provided.
534 535 536 537 538 539 540 |
# File 'lib/plumb/composable.rb', line 534 def (data = Undefined) if data == Undefined MetadataVisitor.call(self) else Metadata.new(self, data) end end |
#not(other = self) ⇒ Not
Negate the result of a step. Ie. if the step is valid, it will be invalid, and vice versa.
550 551 552 |
# File 'lib/plumb/composable.rb', line 550 def not(other = self) Not.new(other) end |
#output_type ⇒ Object
291 |
# File 'lib/plumb/composable.rb', line 291 def output_type = self |
#pipeline(&block) ⇒ Pipeline
Build a Plumb::Pipeline with this object as the starting step. end
837 838 839 |
# File 'lib/plumb/composable.rb', line 837 def pipeline(&block) Pipeline.new(type: self, &block) end |
#policy(*args, &blk) ⇒ Composable
Register a policy for this step. Mode 1.a: #policy(:name, arg) a single policy with an argument Mode 1.b: #policy(:name) a single policy without an argument Mode 2: #policy(p1: value, p2: value) multiple policies with arguments The latter mode will be expanded to multiple #policy calls.
717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 |
# File 'lib/plumb/composable.rb', line 717 def policy(*args, &blk) case args in [::Symbol => name, *rest] # #policy(:name, arg) types = Plumb.resolve_base_types(output_type).uniq = [self] arg = Undefined if rest.size.positive? << rest.first arg = rest.first end block = Plumb.policies.get(types, name) pol = block.call(*, &blk) Policy.new(name, arg, pol) in [::Hash => opts] # #policy(p1: value, p2: value) opts.reduce(self) { |step, (name, value)| step.policy(name, value) } else raise ArgumentError, "expected a symbol or hash, got #{args.inspect}" end end |
#static(value) ⇒ And
Always return a static value, regardless of the input.
807 808 809 |
# File 'lib/plumb/composable.rb', line 807 def static(value) StaticClass.new(value) >> self end |
#subtype_identity ⇒ Object
The type that carries this node's identity for the subtype relation. A
value-preserving type IS its own identity (the default). A value-converting
type is identified by what it produces, so it projects onto a DISTINCT
type (see Function#subtype_identity => output_type): Plumb::Subtyping.subtype?
reduces a <= b to produced(a) <= b before consulting the leaf hooks.
CONTRACT: only return a value other than self when that value is a
genuinely different node. Returning self here is a no-op (subtype? guards
with !equal?(self), so it simply won't reduce); returning a node whose own
#subtype_identity loops back would recurse forever. This is the extension
point for building custom transforming types that play well with subtyping
WITHOUT subclassing Function.
320 |
# File 'lib/plumb/composable.rb', line 320 def subtype_identity = self |
#to_json_schema(root: false) ⇒ Hash
847 848 849 |
# File 'lib/plumb/composable.rb', line 847 def to_json_schema(root: false) JSONSchemaVisitor.call(self, root:) end |
#to_mermaid(direction: 'LR') ⇒ String
Render this composition as a Mermaid flowchart. >> becomes sequential
arrows and | becomes a fork. See Plumb::MermaidVisitor.
855 856 857 |
# File 'lib/plumb/composable.rb', line 855 def to_mermaid(direction: 'LR') MermaidVisitor.call(self, direction:) end |
#to_plumb_type(op:, left:) ⇒ Composable
Composition-context resolution hook, consulted on the RIGHT operand of
#>>, #| and #& before wrapping/type-checking — via
Composable.resolve_operand, which any custom operator implementation
should route its operand through. It lets an operand
resolve itself against the composition context: an Encoder picks the
direction to run in from what left produces; a Codec builds an encode
rewrite of left's output. The returned value replaces the operand in
the composition. Default: identity — every ordinary type composes as
itself.
306 |
# File 'lib/plumb/composable.rb', line 306 def to_plumb_type(op:, left:) = self |
#to_s ⇒ Object
841 842 843 |
# File 'lib/plumb/composable.rb', line 841 def to_s inspect end |
#transform(target_type, callable = nil, &block) ⇒ Function
Transform value. Requires specifying the resulting type of the value after transformation. Shorthand: a single conversion symbol (see COERCION_METHODS) expands to a typed transform to the method's result type, using the symbol as the callable. When the input's base Ruby type is known, it also validates that the type actually responds to the method.
483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 |
# File 'lib/plumb/composable.rb', line 483 def transform(target_type, callable = nil, &block) if target_type.is_a?(::Symbol) && callable.nil? && block.nil? && (out = COERCION_METHODS[target_type]) return coercion_transform(target_type, out) end # Explicit output type + a coercion symbol as the callable # (eg. `#transform(::Numeric, :to_f)`). Since the symbol's result type is # known, we can compare it against the declared output at composition time: # - `ret <= target_type` => the produced value is always within the # declared type, so the runtime output check is redundant (guaranteed). # - `ret` and `target_type` disjoint (neither is a subtype of the other) # => no value can be an instance of both, so the transform would reject # EVERY input. That's a composition error, not a runtime one — raise now. # - otherwise (`target_type < ret`, a genuine narrowing) => may or may not # hold at runtime; leave the output check to do its job. if callable.is_a?(::Symbol) && block.nil? && (ret = COERCION_METHODS[callable]) guaranteed = false if target_type.is_a?(::Module) if ret <= target_type guaranteed = true elsif !(target_type <= ret) raise ArgumentError, ":#{callable} produces a #{ret}, which is never a #{target_type}" end end return transform_step(target_type, callable.to_proc, guaranteed:) end transform_step(target_type, callable || block || Plumb::NOOP) end |
#value(val) ⇒ Object
Match a value using #==
Normally you'll build matchers via ``#[], which uses #===. Use this if you want to match against concrete instances of things that respond to #===`
572 573 574 |
# File 'lib/plumb/composable.rb', line 572 def value(val) constrain(ValueClass.new(val)) end |
#value_preserving? ⇒ Boolean
Whether this type returns its input value UNCHANGED on success — a
coreflexive refinement (a pure filter). Lets #| absorb a redundant
branch (Integer | Numeric == Numeric) without dropping a coercion. See
Optimizer.reduce_union, which memoizes this per frozen node in TypeCache.
Default false; refinements opt in, transforms stay false. A covariant
container (Array/Tuple/HashMap) preserves the value exactly when all its
element/child types do — so Array[Integer] >> Array[Numeric] collapses
like the scalar Integer >> Numeric — while a container that reshapes the
value (a filtered map dropping entries, a record dropping undeclared keys)
stays false. A stronger property than #idempotent? (value-preserving ⟹
idempotent).
351 |
# File 'lib/plumb/composable.rb', line 351 def value_preserving? = false |
#where(attrs) ⇒ Object
Check attributes of an object against values, using #===
693 694 695 696 697 698 699 700 701 702 703 |
# File 'lib/plumb/composable.rb', line 693 def where(attrs) unless attrs.is_a?(::Hash) && !attrs.empty? raise ArgumentError, '#where expects a non-empty Hash of attribute => matcher ' \ "(eg. `where(size: 1..10)`), got #{attrs.inspect}" end attrs.reduce(self) do |t, (name, value)| t >> AttributeValueMatch.new(t, name, value) end end |
#with ⇒ Object
User #where instead
706 707 708 709 |
# File 'lib/plumb/composable.rb', line 706 def with(...) warn 'Composable#with() is deprecated. Use #where() instead. #with is reserved to make copies of Data structs' where(...) end |
#|(other) ⇒ Composable
Chain two composable objects together as a disjunction ("or").
When one value-preserving branch subsumes the other (Integer | Numeric,
or X | X), the union absorbs to the wider branch — see
Optimizer.reduce_union. Functions/containers never reduce (they may accept
inputs the survivor rejects), so coercion unions are preserved.
442 443 444 445 446 447 |
# File 'lib/plumb/composable.rb', line 442 def |(other) other = Composable.resolve_operand(other, op: :|, left: self) return self if other.is_a?(NeverClass) # X | Never == X Plumb::Optimizer.rewrite_union(self, other) end |