idioms for dup/clone

P

Paul Brannan

Suppose I have an class that needs to implement its own dup/clone
methods. What is the correct way to write these methods?

One way is to create protected accessors:

class Foo
def initialize(foo, bar)
@foo = foo
@bar = bar
end

def dup
copy = super
copy.foo = @foo.dup
copy.bar = @bar.dup
end

protected
attr_accessor :foo
attr_accessor :bar
end

Another way is to use instance_eval:

class Foo
def dup
copy = super
foo = @foo.dup
bar = @bar.dup
copy.instance_eval do
@foo = foo
@bar = bar
end
end
end

Questions:

1. How else might dup/clone be implemented? (the deep-copy trick using
marshaling is one way, but the goal here is to write a one-level-deep
dup/clone).

2. What is the right way to set the new instance variables in the copy?

3. Should dup be implemented in terms of clone or should clone be
implented in terms of dup (or should they both be implemented
independently or in terms of another function)?

4. When making the copy, is super the right way to make the copy, or
should allocate be used? (allocate doesn't work on 1.6.x, which I
still use heavily, though if it's the right solution, then it's the
right solution).

5. Certain types cannot be dup'd (e.g. NilClass in all versions of Ruby
or Fixnum in 1.8 and later), so the above code will break if I write:

Foo.new(10, 42).dup

Is it possible (or is it even wise) to write a dup or clone function
that works with both value types and container types?

Paul
 
T

ts

P> 3. Should dup be implemented in terms of clone or should clone be
P> implented in terms of dup (or should they both be implemented
P> independently or in terms of another function)?

For 1.6 Kernel#dup is implemented via Kernel#clone

1.8 use #initialize_copy



Guy Decoux
 
R

Robert Klemme

Just some thoughs and code snippets - no general solutions here...

Paul Brannan said:
Suppose I have an class that needs to implement its own dup/clone
methods. What is the correct way to write these methods?

One way is to create protected accessors:

class Foo
def initialize(foo, bar)
@foo = foo
@bar = bar
end

def dup
copy = super
copy.foo = @foo.dup
copy.bar = @bar.dup
end

You need to return 'copy' here.
protected
attr_accessor :foo
attr_accessor :bar
end

Another way is to use instance_eval:

class Foo
def dup
copy = super
foo = @foo.dup
bar = @bar.dup
copy.instance_eval do
@foo = foo
@bar = bar
end

You need to return 'copy' here.
end
end

Questions:

1. How else might dup/clone be implemented? (the deep-copy trick using
marshaling is one way, but the goal here is to write a one-level-deep
dup/clone).

2. What is the right way to set the new instance variables in the copy?

I guess it really depends...
3. Should dup be implemented in terms of clone or should clone be
implented in terms of dup (or should they both be implemented
independently or in terms of another function)?

IMHO neither since #dup and #clone do have different semantics with
respect to freeze. Well, you could do something like this:

class Foo
attr_accessor :foo, :bar

def dup; do_copy( super, :dup ); end
def clone; do_copy( super, :clone ); end

protected
def do_copy(copy, sym)
copy.instance_eval do
@foo = @foo.send sym
@bar = @bar.send sym
end

copy
end
end

but this does not work for #clone if the instance is frozen.

This might be better:

class Foo
attr_accessor :foo, :bar

def clone; copy :clone; end
def dup; copy :dup; end

protected
def copy(sym)
c = self.class.new

copy_init c

c.freeze? if frozen
c.taint if tainted?

c
end

def copy_init(c)
c.instance_eval do
@foo = @foo.sent sym
@bar = @bar.sent sym
end
end
end

class Bar < Foo
attr_accessor :name

protected
def copy_init(c)
super
c.instance_eval do
@name = @name.send sym
end
end
end

4. When making the copy, is super the right way to make the copy, or
should allocate be used? (allocate doesn't work on 1.6.x, which I
still use heavily, though if it's the right solution, then it's the
right solution).

I'm inclined to follow the Java clone() pattern and use super. Of course
you can also just do self.class.new.
5. Certain types cannot be dup'd (e.g. NilClass in all versions of Ruby
or Fixnum in 1.8 and later), so the above code will break if I write:

Foo.new(10, 42).dup

Is it possible (or is it even wise) to write a dup or clone function
that works with both value types and container types?

It depends on whether you expect these types as members and what you want
to be done with them. It's difficult to anwer this generally, that's why
the automatic methods just do a shallow copy.

Generally speaking IMHO NilClass#dup should return self, same for Fixnums
and others. But OTOH I can see why Matz did it this way: so you get to
know that some instance is not cloneable.

Kind regards

robert
 
R

Robert Klemme

ts said:
P> 3. Should dup be implemented in terms of clone or should clone be
P> implented in terms of dup (or should they both be implemented
P> independently or in terms of another function)?

For 1.6 Kernel#dup is implemented via Kernel#clone

1.8 use #initialize_copy

It does even seem to work with frozen instances. Good to know. Thanks for
that hint!

Regards

robert
 
C

Christoph

ts wrote:

....
P> 3. Should dup be implemented in terms of clone or should clone be
P> implented in terms of dup (or should they both be implemented
P> independently or in terms of another function)?

For 1.6 Kernel#dup is implemented via Kernel#clone

1.8 use #initialize_copy

When redefining #initialize_copy it might be vice or
a virtue to call or not to call or when to call "super" -
for example

---
class AccessHash < Hash
def initialize
@accessed = 0
super
end
def [](key)
@accessed += 1
super
end

def out
p self, @accessed
end

protected
attr_reader :accessed

# first try
def initialize_copy(orig)
@accessed = 0 # reset @accessed
end
end

h = AccessHash.new
h[1] = 1
h[2]
h[1]


h.clone.out # {} - uups, empty AccessHash
# 0

class AccessHash < Hash
def initialize_copy(orig)
super
end
end

h.clone.out # {1=>1}
# 2 - uups, @accessed is wrong

class AccessHash < Hash
def initialize_copy(orig)
@accessed = 0
super
end
end

h.clone.out # {1=>1}
# 0 - works, interesting!!!

# okay just checking

class AccessHash < Hash
def initialize_copy(orig)
super
@accessed = 0
end
end

h.clone.out # {1=>1}
# 0
 
P

Paul Brannan

You need to return 'copy' here.

Good catch, thanks.
IMHO neither since #dup and #clone do have different semantics with
respect to freeze. Well, you could do something like this:

class Foo
attr_accessor :foo, :bar

def dup; do_copy( super, :dup ); end
def clone; do_copy( super, :clone ); end

protected
def do_copy(copy, sym)
copy.instance_eval do
@foo = @foo.send sym
@bar = @bar.send sym
end

copy
end
end

I like this, since it works on both 1.6 and 1.8.
but this does not work for #clone if the instance is frozen.

Right, because the copy has already been frozen when we start to modify
it. I wonder if there's a good way around that (without using
initialize_copy, since that doesn't work on 1.6). Maybe I should
re-implement clone/dup in ruby (as you've done below) for Object so that
it has the same behavior as 1.8.
This might be better:

It's much better, though it seems a bit heavyweight.
class Foo
attr_accessor :foo, :bar

def clone; copy :clone; end
def dup; copy :dup; end

protected
def copy(sym)
c = self.class.new

copy_init c

c.freeze? if frozen
c.taint if tainted?

I think this should read:

if sym == :clone then
c.freeze if frozen?
c.taint if tainted?
end
c
end

def copy_init(c)
c.instance_eval do
@foo = @foo.sent sym
@bar = @bar.sent sym
end
end
end

class Bar < Foo
attr_accessor :name

protected
def copy_init(c)
super
c.instance_eval do
@name = @name.send sym
end
end
end



I'm inclined to follow the Java clone() pattern and use super. Of course
you can also just do self.class.new.

The problem with self.class.new is that the initialize method may have
some semantics that are undesirable when copying.
It depends on whether you expect these types as members and what you want
to be done with them. It's difficult to anwer this generally, that's why
the automatic methods just do a shallow copy.

Generally speaking IMHO NilClass#dup should return self, same for Fixnums
and others. But OTOH I can see why Matz did it this way: so you get to
know that some instance is not cloneable.

That's exactly what I was thinking, though I was hoping for a better
answer.

Paul
 
R

Robert Klemme

Paul Brannan said:
Good catch, thanks.


I like this, since it works on both 1.6 and 1.8.


Right, because the copy has already been frozen when we start to modify
it. I wonder if there's a good way around that (without using
initialize_copy, since that doesn't work on 1.6). Maybe I should
re-implement clone/dup in ruby (as you've done below) for Object so that
it has the same behavior as 1.8.

Well, you could reimplement #clone in terms of #dup and apply all changes
(freeze and taint) aftewards. You'll be quite likely to end up with a
similar scheme to #initialize_copy, so the question is whether it's
worthwile.
It's much better, though it seems a bit heavyweight.


I think this should read:

if sym == :clone then
c.freeze if frozen?
c.taint if tainted?
end

Yes, of course! Thank you!
The problem with self.class.new is that the initialize method may have
some semantics that are undesirable when copying.
Exactly.


That's exactly what I was thinking, though I was hoping for a better
answer.

:)

Maybe Matz has some other reason.

Kind regards

robert
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,768
Messages
2,569,575
Members
45,053
Latest member
billing-software

Latest Threads

Top