Issue #12092 has been updated by Jeremy Evans.

File 0001-Allow-clone-to-take-a-second-argument-passed-to-init.patch added

Since this will be discussed tomorrow at the developers meeting, here's a more detailed example of how this can be used, along with pros and cons of alternative approaches.

Let's say you have a class where each instance has an options hash, which you would like to be immutable (both the instance and the underlying options hash).  You would like to created modified clones of this object, where the clones are also immutable but copy the singleton classes from the original object.  With the patch attached to this feature request, you would have to write code like:

~~~
A = Struct.new(:opts)
a = A.new({}.freeze).extend(SomeModule).freeze
hash = {:c=>1}
a.clone{|b| b.opts = b.opts.merge(hash).freeze}
~~~

The attached patch was designed to be the minimally invasive change that supports the need to created modified copies of objects that are frozen and have singleton classes.  However, it's not necessarily the best approach.

Alternative approach #1: Have #clone pass a block given to #initialize_clone. 

Example:

~~~
A = Struct.new(:opts) do
  def initialize_clone(clone)
    clone.opts = clone.opts.dup
    yield clone
    clone.opts.freeze
    super
  end
end

a = A.new({}.freeze).extend(SomeModule).freeze
hash = {:c=>1}

# Not possible in attached patch as #clone yields after #initialize_clone,
# and clone.opts would already be frozen in that case
a.clone{|b| b.opts.merge!(hash)}
~~~
  
Pros:

* Doesn't change current behavior when passing #clone a block. Blocks passed to #clone will not be yielded to, unless the object's #initialize_clone has been overriden to yield to the block.
* Allows users to determine when to yield, as they may want to yield before doing some work in #initialize_clone.

Cons:

* Requires overriding #initialize_clone for each class that you want to be able to modify during #clone.
* Possible additional runtime overhead unless proc activation can be avoided.
* Requires more changes to the existing code.

Alternative approach #2: Allow #clone to accept an argument to pass to #initialize_clone

Example:

~~~
A = Struct.new(:opts) do
  def initialize_clone(clone, opts)
    clone.opts = self.opts.merge(opts).freeze
    super
  end
end
  
a = A.new({}.freeze).extend(SomeModule).freeze
hash = {:c=>1}

# Much simpler API for this and probably most use cases
a.clone(hash)
~~~

Pros:

* Faster as it doesn't require creating a block at all.
* Simpler for most use cases

Cons:

* Requires overriding #initialize_clone for each class that you want to be able to modify during #clone.
* Requires more changes to the existing code, but I have a working patch for it.

I think alternative approach #2 is probably the best way to support this.  I'm attaching a patch for it as well.

----------------------------------------
Feature #12092: Allow Object#clone to yield cloned object before freezing
https://bugs.ruby-lang.org/issues/12092#change-57420

* Author: Jeremy Evans
* Status: Open
* Priority: Normal
* Assignee: 
----------------------------------------
This allows creating modified clones of frozen objects that have
singleton classes:

~~~
a = [1,2,3]
def a.fl; first + last; end
a.freeze
a.fl # => 4

clone = a.clone{|c| c << 10}
clone.last # => 10
clone.fl # => 11
clone.frozen? # => true
~~~

Previously, this was not possible at all.  If an object was
frozen, the clone was frozen before the cloned object could
be modified.  It was possible to modify the clone using
initialize_clone or initialize_copy, but you couldn't change how
to modify the clone on a per-call basis.  You couldn't use dup
to return an unfrozen copy, modify it, and then freeze it, because
dup doesn't copy singleton classes.

This allows ruby to be used in a functional style with immutable
data structures, while still keeping the advantages of singleton
classes.

---Files--------------------------------
0001-Allow-clone-to-yield-cloned-object-before-freezing.patch (2.51 KB)
0001-Allow-clone-to-take-a-second-argument-passed-to-init.patch (2.37 KB)


-- 
https://bugs.ruby-lang.org/

Unsubscribe: <mailto:ruby-core-request / ruby-lang.org?subject=unsubscribe>
<http://lists.ruby-lang.org/cgi-bin/mailman/options/ruby-core>