Class: Farce::Walker

Inherits:
Object
  • Object
show all
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.

Examples:

Scan a namespace without changing it

namespace = Module.new
namespace.const_set(:VALUE, [42])
Farce::Walker.any?(namespace) { Integer === it } # => true
Farce::Walker.any?(namespace, constants: false) { Integer === it } # => false

Replace integers in a copy

Farce::Walker.modify([1, [2]], copy: true) do |object, walker|
  Integer === object ? object + 1 : walker.traverse
end # => [2, [3]]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#class_variables ⇒ Boolean, Symbol (readonly)

Returns the class variable traversal policy.

Returns:

  • (Boolean, Symbol) —

    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.

Returns:

  • (Boolean, Symbol) —

    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.

Returns:

  • (BasicObject) —

    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.

Returns:

  • (false, true, Symbol) —

    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.

Parameters:

  • constants (Boolean, Symbol) (defaults to: true) —

    true for directly defined public constants, :inherited to include ancestors, or false to skip. Autoloads are not loaded.

  • class_variables (Boolean, Symbol) (defaults to: true) —

    true for directly defined class variables, :inherited to include ancestors, or false to skip

Returns:

  • (Boolean)


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.

Parameters:

  • constants (Boolean, Symbol) (defaults to: true) —

    true for directly defined public constants, :inherited to include ancestors, or false to skip. Autoloads are not loaded.

  • class_variables (Boolean, Symbol) (defaults to: true) —

    true for directly defined class variables, :inherited to include ancestors, or false to skip

Returns:

  • (Boolean)


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

Note:

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).

Examples:

# Lets assume NativeClass has a natively stored value
Farce::Walker.define(NativeClass) do |object, walker|
  walker.update(object, [object.value]) do |target, values|
    target.value = values.first
    target
  end
end

Parameters:

  • classes (Array<Class>) —

    classes to handle

Yield Parameters:

  • object (BasicObject) —

    the object to traverse

  • walker (Walker) —

    the current walk

Yield Returns:

  • (BasicObject) —

    the resulting object

Raises:

  • (LocalJumpError)


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.

Parameters:

  • object (BasicObject) —

    the root object

  • constants (Boolean, Symbol) (defaults to: true) —

    true for directly defined public constants, :inherited to include ancestors, or false to skip. Autoloads are not loaded.

  • class_variables (Boolean, Symbol) (defaults to: true) —

    true for directly defined class variables, :inherited to include ancestors, or false to skip

Yield Parameters:

  • object (BasicObject) —

    a reachable object

Returns:

  • (BasicObject, Enumerator) —

    the root object, or an enumerator without a block



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.

Parameters:

  • object (BasicObject) —

    the root object

  • constants (Boolean, Symbol) (defaults to: true) —

    true for directly defined public constants, :inherited to include ancestors, or false to skip. Autoloads are not loaded.

  • class_variables (Boolean, Symbol) (defaults to: true) —

    true for directly defined class variables, :inherited to include ancestors, or false to skip

  • copy (Boolean, Symbol) (defaults to: false) —

    false to edit mutable objects in place, true to duplicate changed objects, or a copy method such as :clone. Unchanged objects may be shared with the input. Frozen objects are cloned only when changed.

  • freeze (Boolean, nil) (defaults to: nil) —

    true to freeze results, false to avoid freezing them, or nil to preserve each original object's frozen state

Yield Parameters:

  • object (BasicObject) —

    the original object

  • walker (Walker) —

    the current walk

Yield Returns:

  • (BasicObject) —

    the replacement object

Returns:

  • (BasicObject) —

    the transformed root, or the value passed to #return

Raises:

  • (LocalJumpError) —

    if no block is given

  • (ArgumentError) —

    if copy is neither a boolean nor a Symbol

  • (ArgumentError) —

    if cyclic callbacks do not converge within 32 passes, or a cyclic finalizer replaces its result

  • (ArgumentError) —

    if a hash key or set element refers to a Data value still being constructed. Identity-based containers do not hash their 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.

Parameters:

  • object (BasicObject) —

    the root object

Yield Parameters:

Yield Returns:

  • (BasicObject) —

    the result for the current object

Returns:

  • (BasicObject) —

    the root callback's result, or the value passed to #return

Raises:

  • (LocalJumpError) —

    if no block is given



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.

Parameters:

  • object (BasicObject) (defaults to: current_object) —

    the object to inspect

Returns:

  • (Boolean) —

    false for a read-only walk



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.

Yield Parameters:

  • result (BasicObject) —

    the settled result

  • cyclic (Boolean) —

    whether the node belongs to a cycle

Yield Returns:

  • (BasicObject) —

    the final result

Returns:

  • (BasicObject) —

    the current destination

Raises:

  • (ArgumentError) —

    outside a modifying walk

  • (LocalJumpError) —

    if no block is given



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.

Returns:

  • (BasicObject) —

    the current destination

Raises:

  • (ArgumentError) —

    outside a modifying walk



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.

Parameters:

  • object (BasicObject) (defaults to: current_object) —

    the walk's result, defaulting to the current object



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.

Parameters:

  • object (BasicObject) (defaults to: current_object) —

    the replacement, defaulting to the current object



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.

Parameters:

  • object (BasicObject) (defaults to: current_object) —

    the object to descend into, defaulting to the current object

Returns:

  • (BasicObject) —

    the object with transformed children when modifying



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.

Parameters:

  • object (BasicObject) —

    the object being traversed

  • values (Array) —

    child references in assignment order

  • hash_keys (Boolean) (defaults to: false) —

    rebuild when key descendants change in place

  • key_stride (Integer) (defaults to: 1) —

    spacing between keys in values, starting at zero

Yield Parameters:

  • target (BasicObject) —

    the writable destination

  • results (Array) —

    transformed child references

Returns:

  • (BasicObject) —

    the traversal result



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.

Parameters:

  • object (BasicObject) —

    the child object

Returns:

  • (BasicObject) —

    the callback result or an in-progress copy for a cycle



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