Class: Farce::Mutable

Inherits:
BasicObject
Includes:
Shareable::Delegated
Defined in:
lib/farce/mutable.rb

Overview

A wrapper making generic Ruby objects shareable while preserving mutability.

It achieves this by keeping an immutable copy of the object, and performing an atomic update of this reference for mutating method calls.

This means the entire object is copied whenever it is being mutated.

You should therefore favor dedicated data structures whenever possible, like using a Map instead of wrapping a Hash in a Mutable, or a Vector instead of doing the same with an Array.

Non-mutating calls do not incur this cost.

Use a transaction view to stage mutations alongside changes to other participants. The original value changes only when the transaction commits.

Examples:

Updating two strings together

first  = Farce::Mutable.new("queued")
second = Farce::Mutable.new("waiting")
Farce.transaction(first, second) do |_, first_view, second_view|
  first_view.replace("running")
  second_view << " for worker"
end
shareable_string = Farce::Mutable.new("foo")
Ractor.new(shareable_string) { it << "bar" }.join
shareable_string.to_s # => "foobar"

Direct Known Subclasses

Transaction::Mutable

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Shareable

#ractor_shareable?

Constructor Details

#initialize(object) ⇒ Mutable

Creates a new mutable version of the given object. The object will be copied on initialization if it isn't frozen. A frozen version of the object must be Ractor-shareable.

Parameters:

  • object (BasicObject) —

    The object to wrap. Must implement dup, freeze, and frozen?.



103
104
105
106
# File 'lib/farce/mutable.rb', line 103

def initialize(object)
  @atom = Internal::Atom.new Mutator.prepare(object)
  super()
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing ⇒ BasicObject (private)

Delegates all methods to the frozen copy of the current value. If it throws a FrozenError it reruns the method against an unfrozen duplicate of the value, freezes it, and uses that as internal value.



141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/farce/mutable.rb', line 141

def method_missing(...) # rubocop:disable Style/MissingRespondToMissing
  @atom.value.__send__(...)
rescue ::FrozenError
  result = nil
  @atom.update do |current|
    copy   = current.dup
    result = copy.__send__(...)
    result = self if result.equal?(copy)
    Mutator.prepare(copy, in_place: true)
  end
  result
end

Class Method Details

.[](factory) ⇒ Class

Creates a subclass of Farce::Mutable that automatically generates values based on the given factory (usually a class), by invoking new on it with any arguments being passed on.

Examples:

# You should probably use Farce::Vector instead.
MutableArray = Farce::Mutable[Array]
MutableArray.new(2) # => #<MutableArray [nil, nil]>

Parameters:

  • factory (#new) —

    The factory to create a subclass for. Typically a class. Must be shareable.

Returns:

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/farce/mutable.rb', line 80

def self.[](factory)
  raise ::Farce::Ractor::IsolationError, "factory is not shareable" unless ::Farce::Ractor.shareable?(factory)

  klass = ::Class.new(self)
  klass.set_temporary_name("#{name}[#{factory.name}]")
  klass.instance_variable_set(:@value_factory, factory)

  klass.class_eval <<~RUBY, __FILE__, __LINE__ + 1
    def self.new(...) = super(value_factory.new(...))
    def self.value_factory
      return @value_factory if defined?(@value_factory) && @value_factory
      superclass.value_factory
    end
  RUBY

  klass.singleton_class.class_eval "undef []", __FILE__, __LINE__
  klass
end

.deref(mutable) ⇒ BasicObject

Turns the given Farce::Mutable into a frozen copy of the underlying object.

Parameters:

  • mutable (Mutable) —

    the mutable

Returns:

  • (BasicObject) —

    current snapshot of the object it is wrapping



68
# File 'lib/farce/mutable.rb', line 68

def self.deref(mutable) = Mutable === mutable ? Mutator.deref(mutable) : mutable

Instance Method Details

#respond_to?(name, include_private = false) ⇒ Boolean

Marshal hook discovery must inspect the wrapper without delegating to its snapshot.

Returns:

  • (Boolean)


115
116
117
118
119
# File 'lib/farce/mutable.rb', line 115

def respond_to?(name, include_private = false) # rubocop:disable Style/OptionalBooleanParameter
  return true if name == :marshal_dump || name == :transaction_wrapper
  return false if Internal.marshal_protocol_method?(name)
  method_missing(:respond_to?, name, include_private)
end