Class: Farce::Walker
- Inherits:
-
Object
- Object
- Farce::Walker
- Includes:
- Internal::MarshalSupport::Reject
- Defined in:
- lib/farce/walker.rb,
lib/farce/walker/definitions.rb,
lib/farce/walker/modification.rb
Overview
Visit an object graph or transform it with copies only where needed.
Traversal follows container elements, hash keys and values, and ordinary instance variables. Farce containers expose their contents, not their storage. Proc traversal follows the receiver, not captured local variables. Module and class traversal includes directly defined public constants and class variables. Set constants: :inherited or class_variables: :inherited to include ancestors, or false to skip either kind. Autoloads are not loaded.
Walker.visit and Walker.modify let the callback choose when to descend with #traverse. Walker.each descends automatically and yields children before their parent. Shared children and cycles are tracked by identity.
Instance Attribute Summary collapse
-
#class_variables ⇒ Boolean, Symbol
readonly
The class variable traversal policy.
-
#constants ⇒ Boolean, Symbol
readonly
The constant traversal policy.
-
#current_object ⇒ BasicObject
readonly
The original object currently passed to the callback.
-
#modify ⇒ false, ...
readonly
False for a read-only walk, true for in-place edits, or the selected copy method.
Class Method Summary collapse
-
.all?(object, constants: true, class_variables: true) ⇒ Boolean
Stop at the first object for which the block is falsey.
-
.any?(object, constants: true, class_variables: true) ⇒ Boolean
Stop at the first object for which the block is truthy.
-
.define(*classes) {|object, walker| ... } ⇒ BasicObject
Register traversal for classes from the main Ractor.
-
.each(object, constants: true, class_variables: true) {|object| ... } ⇒ BasicObject, Enumerator
Yield each reachable object once, after its children.
-
.modify(object, copy: false, freeze: nil, constants: true, class_variables: true) {|object, walker| ... } ⇒ BasicObject
Transform a graph using the callback's return values.
-
.visit(object, constants: true, class_variables: true) {|object, walker| ... } ⇒ BasicObject
Visit an object without automatically descending or assigning replacements.
Instance Method Summary collapse
-
#changed?(object = current_object) ⇒ Boolean
Whether a visited object or its traversed descendants changed.
-
#finalize {|result, cyclic| ... } ⇒ BasicObject
Register a finalizer for the current node.
-
#freeze_result ⇒ BasicObject
Request freezing after the current result is settled.
-
#return(object = current_object)
End the whole walk immediately.
-
#skip(object = current_object)
End the current callback and use object as its result without freezing it.
-
#traverse(object = current_object) ⇒ BasicObject
Traverse an object's children using its registered class definition.
-
#update(object, values, hash_keys: false, key_stride: 1) {|target, results| ... } ⇒ BasicObject
Visit child values and describe how to assign their replacements.
-
#visit(object) ⇒ BasicObject
Visit a child, reusing the result if its identity has already been seen.
Instance Attribute Details
#class_variables ⇒ Boolean, Symbol (readonly)
Returns the class variable traversal policy.
198 199 200 |
# File 'lib/farce/walker.rb', line 198 def class_variables @class_variables end |
#constants ⇒ Boolean, Symbol (readonly)
Returns the constant traversal policy.
195 196 197 |
# File 'lib/farce/walker.rb', line 195 def constants @constants end |
#current_object ⇒ BasicObject (readonly)
Returns the original object currently passed to the callback.
201 202 203 |
# File 'lib/farce/walker.rb', line 201 def current_object @current_object end |
#modify ⇒ false, ... (readonly)
Returns false for a read-only walk, true for in-place edits, or the selected copy method.
192 193 194 |
# File 'lib/farce/walker.rb', line 192 def modify @modify end |
Class Method Details
.all?(object, constants: true, class_variables: true) ⇒ Boolean
Stop at the first object for which the block is falsey. Without a block, test the objects themselves.
138 139 140 |
# File 'lib/farce/walker.rb', line 138 def self.all?(object, constants: true, class_variables: true, &) each(object, constants:, class_variables:).all?(&) end |
.any?(object, constants: true, class_variables: true) ⇒ Boolean
Stop at the first object for which the block is truthy. Without a block, test the objects themselves.
130 131 132 |
# File 'lib/farce/walker.rb', line 130 def self.any?(object, constants: true, class_variables: true, &) each(object, constants:, class_variables:).any?(&) end |
.define(*classes) {|object, walker| ... } ⇒ BasicObject
The block must be convertible to a shareable proc.
Register traversal for classes from the main Ractor.
Subclasses inherit the nearest definition. Definitions can call super.
Use #update to visit children and record assignments. Its block receives
a writable target and transformed children. Return that target from the
assignment block and return the update result from the definition.
Should only be necessary for classes using storage defined outside of Ruby (like a C struct).
89 90 91 92 93 |
# File 'lib/farce/walker.rb', line 89 def self.define(*classes, &) definition = Internal.prepare_method_definition(&) raise LocalJumpError, "no block given" unless definition classes.each { REGISTER.define(it) { define_method(:traverse, definition) } } end |
.each(object, constants: true, class_variables: true) {|object| ... } ⇒ BasicObject, Enumerator
Yield each reachable object once, after its children.
117 118 119 120 121 122 123 124 |
# File 'lib/farce/walker.rb', line 117 def self.each(object, constants: true, class_variables: true) return enum_for(:each, object, constants:, class_variables:) unless block_given? visit(object, constants:, class_variables:) do |object, walker| walker.traverse(object) yield(object) object end end |
.modify(object, copy: false, freeze: nil, constants: true, class_variables: true) {|object, walker| ... } ⇒ BasicObject
Transform a graph using the callback's return values. Call #traverse to transform an object's children. Returning another value replaces the object without automatically visiting that replacement. Unchanged branches retain their identity, including when copy is enabled. Changed cycles are connected before results are frozen or finalized. A cyclic callback may run again as child replacements become known. Keep callbacks repeatable and put publication or caching in #finalize. Use #freeze_result instead of freezing a preliminary traversal result. Data values are rebuilt only when members change, using their normal initializer. Noncopyable coordination objects, such as leases, are updated in place. Constants and class variables are assigned only when their value changes identity. Constant replacement can emit Ruby redefinition warnings. Replacing an inherited constant defines a local constant. Replacing an inherited class variable updates storage shared with its owner and siblings.
A walk is not an atomic snapshot or update. Coordinate concurrent writers when transforming container structure, especially map keys.
176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/farce/walker.rb', line 176 def self.modify(object, copy: false, freeze: nil, constants: true, class_variables: true, &callback) raise LocalJumpError, "no block given" unless callback modify = case copy when false then true when true then :dup when Symbol then copy else raise ArgumentError, "invalid value for copy: #{copy.inspect}" end catch(RETURN) { Modification.new(callback, modify, freeze, constants, class_variables).visit(object) } end |
.visit(object, constants: true, class_variables: true) {|object, walker| ... } ⇒ BasicObject
Visit an object without automatically descending or assigning replacements.
107 108 109 110 |
# File 'lib/farce/walker.rb', line 107 def self.visit(object, constants: true, class_variables: true, &callback) raise LocalJumpError, "no block given" unless callback catch(RETURN) { new(callback, false, false, constants, class_variables).visit(object) } end |
Instance Method Details
#changed?(object = current_object) ⇒ Boolean
Whether a visited object or its traversed descendants changed.
308 |
# File 'lib/farce/walker.rb', line 308 def changed?(object = current_object) = false # rubocop:disable Lint/UnusedMethodArgument |
#finalize {|result, cyclic| ... } ⇒ BasicObject
Register a finalizer for the current node. It runs once after resolution and requested freezing. Registering another finalizer replaces the first. Cyclic finalizers must preserve identity. Acyclic finalizers may return a canonical replacement, which is propagated to the parent.
297 |
# File 'lib/farce/walker.rb', line 297 def finalize(&) = raise(ArgumentError, "finalize requires a modifying walk") |
#freeze_result ⇒ BasicObject
Request freezing after the current result is settled. With copying enabled, mutable inputs are copied before freezing.
285 |
# File 'lib/farce/walker.rb', line 285 def freeze_result = raise(ArgumentError, "freeze_result requires a modifying walk") |
#return(object = current_object)
This method returns an undefined value.
End the whole walk immediately.
251 |
# File 'lib/farce/walker.rb', line 251 def return(object = current_object) = throw(RETURN, object) |
#skip(object = current_object)
This method returns an undefined value.
End the current callback and use object as its result without freezing it.
246 |
# File 'lib/farce/walker.rb', line 246 def skip(object = current_object) = throw(SKIP, object) |
#traverse(object = current_object) ⇒ BasicObject
Traverse an object's children using its registered class definition.
256 257 258 259 260 |
# File 'lib/farce/walker.rb', line 256 def traverse(object = current_object) klass = send_to(object, :class) visitor = @visitors[klass] ||= REGISTER[klass] visitor.call(object, self) end |
#update(object, values, hash_keys: false, key_stride: 1) {|target, results| ... } ⇒ BasicObject
Visit child values and describe how to assign their replacements. The assignment block only runs during modification and may run again for cycles. It receives a writable target and the resolved children. Do not mutate object outside this block. Return the result of update from a traversal definition.
276 277 278 279 |
# File 'lib/farce/walker.rb', line 276 def update(object, values, hash_keys: false, key_stride: 1) # rubocop:disable Lint/UnusedMethodArgument values.each { visit(it) } object end |
#visit(object) ⇒ BasicObject
Visit a child, reusing the result if its identity has already been seen.
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 |
# File 'lib/farce/walker.rb', line 223 def visit(object) @seen.fetch(object) do current_object = @current_object @current_object = object @seen[object] = object # set this first to stop recursion @seen[object] = catch(SKIP) do result = @callback.call(object, self) case @modify && @freeze when false then result when true then send_to(result, :freeze) else send_to(result, :freeze) if send_to(object, :frozen?) && !send_to(result, :frozen?) result end end ensure @current_object = current_object end end |