Correct way to copy objects

In the next example I am creating several objects/subobjects for an object called structA.
I also create an object called structB and copy subobjects from structA to structB.

The problem I think I am facing is that the dup method does a shallow copy. Ideally I want a full copy of the subobjects of structA. I read I should use the clone method.

From Object - Crystal 1.0.0

create a deep copy (#clone): Constructs a new object with all its properties’ values being recursive deep copies of the original object’s properties. There is no shared state and the new object is a completely independent copy, including everything inside it. This may not be available for every type.

However this gives an error

There was a problem expanding macro 'macro_139949861143760'

Code in /usr/share/crystal/src/array.cr:696:5

 696 | {% if T == ::Bool || T == ::Char || T == ::String || T == ::Symbol || T < ::Number::Primitive %}
       ^
Called macro defined in /usr/share/crystal/src/array.cr:696:5

 696 | {% if T == ::Bool || T == ::Char || T == ::String || T == ::Symbol || T < ::Number::Primitive %}

Which expanded to:

 > 4 | hash[object_id] = clone.object_id
 > 5 | each do |element|
 > 6 |   clone << element.clone.as(T)
                          ^----
Error: undefined method 'clone' for Node (compile-time type is Node+)

Example code


class Node
  property name : String = ""
  property nodes : Array(Node)

  def initialize(name : String)
    @name = name
    @nodes = [] of Node
  end

  def to_s
    "address #{self} #{@name}"
  end
end

class StructDef < Node
end

class FunctionDef < Node
end

class FunctionCall < Node
end

# A
structA = StructDef.new("A")

# struct B
structB = StructDef.new("B")

functiondef1 = FunctionDef.new("f1")
functiondef2 = FunctionDef.new("f2")

functioncall1 = FunctionCall.new("f1")
functioncall2 = FunctionCall.new("f2")

functiondef1.nodes << functioncall1
functiondef2.nodes << functioncall2

structA.nodes << functiondef1
structA.nodes << functiondef2


#
# dump A
#
puts "struct A #{structA.to_s}"
structA.nodes.each do |n|
  puts "\tnode #{n.to_s}"

  n.nodes.each do |x|
    puts "\t\tsubnode #{x} name #{x.name}"
  end
end

puts
puts "copy nodes from A to B"
puts

#
# The addresses of B nodes are the same as the addresses of A nodes
# when using dup. Try the clone method instead
#
structB.nodes = structA.nodes.clone #nodes.dup

#
# dump B
#
puts "struct B #{structB.to_s}"
structB.nodes.each do |n|
  puts "\tnode #{n.to_s}"

  n.nodes.each do |x|
    puts "\t\tsubnodes #{x} name #{x.name}"
  end
end
Crystal 1.0.0 [dd40a2442] (2021-03-22)

LLVM: 10.0.0
Default target: x86_64-unknown-linux-gnu
1 Like

Clone is not defined by default on every object. You need to define clone on Node and potentially on each subclass. You can use the def_clone macro to generate it.

1 Like

Updated the example and now using the macro def_clone and the method clone.

class Node
  property name : String = ""
  property nodes : Array(Node)

  def initialize(name : String)
    @name = name
    @nodes = [] of Node
  end

  def to_s
    "address #{self} #{@name}"
  end

  def_clone
end

class StructDef < Node
  def_clone
end

class FunctionDef < Node
  def_clone
end

class FunctionCall < Node
  def_clone
end

# A
structA = StructDef.new("A")

# struct B
structB = StructDef.new("B")

functiondef1 = FunctionDef.new("f1")
functiondef2 = FunctionDef.new("f2")

functioncall1 = FunctionCall.new("f1")
functioncall2 = FunctionCall.new("f2")

functiondef1.nodes << functioncall1
functiondef2.nodes << functioncall2

structA.nodes << functiondef1
structA.nodes << functiondef2

#
# dump A
#
puts "struct A #{structA.to_s}"
structA.nodes.each do |n|
  puts "\tnode #{n.to_s}"

  n.nodes.each do |x|
    puts "\t\tsubnode #{x} name #{x.name}"
  end
end

puts
puts "copy nodes from A to B"
puts

structB.nodes = structA.nodes.clone 

#
# dump B
#
puts "struct B #{structB.to_s}"
structB.nodes.each do |n|
  puts "\tnode #{n.to_s}"

  n.nodes.each do |x|
    puts "\t\tsubnodes #{x} name #{x.name}"
  end
end

The mystery remains. Did it work? :grin:

I could not give an update earlier as I was busy with other things but to answer your question,
yes, it worked. Mystery solved and thanks for your help.

1 Like