-1

I am a newbie. so far I have been in touch with two languages, ruby and python. I also read a couple of essays about C++ pointers, I had a bit of confusion with Array Objects and how to manipulate the value inside. I found that including an exclamation mark in the method, can change the array without assignment. however sometimes it doesn't; I appreciate it if someone can explain the nature of array, what are they, are they a location to store values, or are they the collection of values? more specifically, if I divide ruby array functions into two groups: the one that can change the values of array without assignment directly, and the ones who need assignment, could you please give me a couple of examples
In the following example, I don't understand why the values of my initial array is changed.
here is my code example:

edges = [[[1, 2], [100, 200]], [[700, 400], [1000, 2000]]]

new_edges = Array.new(edges)
new_edges.map{|edge| edge.map{|point| point.insert(0, 808912)}}

edges
#=> [[[808912, 1, 2], [808912, 100, 200]], [[808912, 700, 400], [808912, 1000, 2000]]]

new_edges
#=> [[[808912, 1, 2], [808912, 100, 200]], [[808912, 700, 400], [808912, 1000, 2000]]]

what I believe will happen, is that values in edges won't be changed, and the new_edges will be changed. I do appreciate for your help.

Weilory
  • 2,621
  • 19
  • 35
  • It looks like you are creating `new_edges` array twice. Once with Array.new, and Twice with new_edges.map. Why are you doing it this way? What happened when you rant this code in terminal? – Mr. J Nov 08 '19 at 02:33
  • is there a better way of doing this? Could you please give me some suggestions, thanks. – Weilory Nov 08 '19 at 02:34
  • Well Array.new(edges) seems okay... but what's the problem? You need to include the results when you run this code, ie. error message, or the output. – Mr. J Nov 08 '19 at 02:36
  • Possible duplicate of [How to initialize an array in one step using Ruby?](https://stackoverflow.com/questions/4908413/how-to-initialize-an-array-in-one-step-using-ruby) – Mr. J Nov 08 '19 at 02:37
  • what it is initially: [[[1, 2], [100, 200]], [[700, 400], [1000, 2000]]], what I want: [[[808912, 1, 2], [808912, 100, 200]], [[808912, 700, 400], [808912, 1000, 2000]]], and I need to use the initial value later – Weilory Nov 08 '19 at 02:38
  • the first puts result from terminal is: [[[808912, 1, 2], [808912, 100, 200]], [[808912, 700, 400], [808912, 1000, 2000]]]. the second puts result is: [[[808912, 1, 2], [808912, 100, 200]], [[808912, 700, 400], [808912, 1000, 2000]]] – Weilory Nov 08 '19 at 02:42
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/202034/discussion-between-messiz-qin-and-james-h). – Weilory Nov 08 '19 at 02:58

3 Answers3

7

Your code has the same problems as this would in Python:

edges = [[[1, 2], [100, 200]], [[700, 400], [1000, 2000]]]
new_edges = list(edges)
for edge in edges:
    for point in edge:
        point.insert(0, 808912)
print(edges)

I.e. you transform the outer array, but the inner arrays are only held by reference, and thus changing one (by insert) is changing the deep content of both edges and new_edges. These kinds of problems are very easily understood by stepping through your code with this tool (despite the name, it works for both Python and Ruby).

In Ruby, instead of using insert, which modifies an array, you can use +, which does not:

edges = [[[1, 2], [100, 200]], [[700, 400], [1000, 2000]]]
new_edges = edges.map { |edge| edge.map { |point| [808912] + point } }
# => [[[808912, 1, 2], [808912, 100, 200]], [[808912, 700, 400], [808912, 1000, 2000]]]
edges
# => [[[1, 2], [100, 200]], [[700, 400], [1000, 2000]]]
Amadan
  • 191,408
  • 23
  • 240
  • 301
  • Thank you. I also find another way: 'new_edges = Marshal.load(Marshal.dump(edges))' 'new_edges.map{|point| point.insert(0, 808912)}' – Weilory Nov 09 '19 at 00:11
3

Here is a more general situation that illustrates the problem you are having and ways of correcting it. Suppose we had the following nested arrays:

a0 = [1, 2]
a1 = [3, 4]
a = [a0, a1]
  #=> [[1, 2], [3, 4]] 
edges = [a]
  #=> [[[1, 2], [3, 4]]] 

a0, a1, a and edges have unique object ids:

edges.object_id           #=> 1208140 

a.object_id               #=> 1085620  
edges[0].object_id        #=> 1085620 

a0.object_id              #=> 0977080 
a[0].object_id            #=> 0977080
edges[0][0].object_id     #=> 0977080

a1.object_id              #=> 0995980 
a[1].object_id            #=> 0995980
edges[0][1].object_id     #=> 0995980
edges[0][1][0].object_id  #=> 7

For readability I have removed the first seven digits of each of the object ids, which in all cases is 4833847. Note edges[0][1][0] #=> 3 and 3.object_id #=> 7. For reasons of efficiency, integers (and certain other Ruby objects have fixed, smallish, object ids.

Now create a new array from edges using the method Array::new:

new_edges = Array.new(edges)
  #=> [[[1, 2], [3, 4]]]

Examine the (last six digits of the) object ids:

new_edges.object_id          #=> 2400460 (different than edges.object_id)
new_edges[0].object_id       #=> 1085620 (same as edges[0].object_id)
new_edges[0][0].object_id    #=> 0977080 (same as edges[0][0].object_id)
new_edges[0][1].object_id    #=> 0995980 (same as edges[0][1].object_id)
new_edges[0][1][0].object_id #=> 7       (same as edges[0][1][0].object_id)    

It is seen that new_edges is a new object, but all of its nested arrays and elements are the same objects as the corresponding nested arrays and elements in edges.

Now let's do the following:

edges[0][1][0] = 5
edges[0][1][0].object_id #=> 11

Then

edges
  #=> [[[1, 2], [5, 4]]] 
new_edges 
  #=> [[[1, 2], [5, 4]]]

new_edges was changed as well as edges because edges[0][1] and new_edges[0][1] are the same object (array) and we just changed the first element of that object.

How do we avoid changing new_edges when edges is changed?

Firstly, note that new_edges = Array.new(edges) can be replaced with new_edges = edges.dup. As before, edges and new_edges will be different objects but all of their corresponding nested arrays will the same objects.

We wish to define new_edges by making a deep copy of edges, so that changes to the latter will not affect the former, and vice-versa:

new_edges = edges.map { |a| a.map { |aa| aa.dup } }  
  #=> [[[1, 2], [3, 4]]]
new_edges.object_id       #=> 2134620 (different than edges.object_id) 
new_edges[0].object_id    #=> 2134600 (different than edges[0].object_id)
new_edges[0][0].object_id #=> 2134580 (different than edges[0][0].object_id)
new_edges[0][1].object_id #=> 2134560 (different than edges[0][1].object_id)

Now change a nested element in edges and observe the values of edges and new_edges:

edges[0][1][0] = 5

edges
  #=> [[[1, 2], [5, 4]]] 
new_edges
  #=> [[[1, 2], [3, 4]]]

It is seen that new_edges is not modified.

If there are greater levels of nesting it can become tedious and error-prone to make a deep copy using map and dup. An easier way is to use Marshal#dump and Marshal#load, which create deep copies of a wide range of Ruby objects that may contain multiple levels of nested objects:

edges
  #=> [[[1, 2], [5, 4]]] 
new_edges = Marshal.load(Marshal.dump(edges))
  #=> [[[1, 2], [5, 4]]]

Changes to edges will now leave new_edges unaffected.

edges[0][1][0] = 3

edges
  #=> [[[1, 2], [3, 4]]] 
new_edges
  #=> [[[1, 2], [5, 4]]] 
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
1

You start out with

edges = [[[1, 2], [100, 200]], [[700, 400], [1000, 2000]]]

Dup won't work here since it's a nested array, so you need to deep_dup it before doing what you wanna do. (If you don't want to mutate the original array)

Here's an example of a monkeypatched deep_dup (Array)

  def deep_dup
    new_arr = []
    self.each do |ele|
      if ele.is_a? Array
        new_arr << ele.deep_dup
      else
        new_arr << ele
      end
    end
   new_arr
  end

You can obv. do this a lot cleaner but whatever

EDIT: Array has a built-in deep_dup method, use that. I always thought Ruby left deep_dup up to implementation, but I was wrong.

TedTran2019
  • 887
  • 5
  • 15
  • 1
    Ruby doesn't have `deep_dup`. ActiveSupport does (and thus Rails does as well). – Amadan Nov 08 '19 at 04:33
  • Oh, I just googled it and must have misread the documentation. My bad, I just saw #deep_dup, but it must have been ActiveSupport and not Ruby. Also, your solution is way more space and time effective, haha. Just using a method that doesn't mutate the original array is a lot better than deep_dupping the original then using methods that do mutate. – TedTran2019 Nov 08 '19 at 05:07
  • 1
    A somewhat hackish way to deep clone an array without using Rails is `Marshal.load Marshal.dump(my_array)`. `Marshal.dump` packs the array into a string format, which `Marshal.load` unpacks into an array. – BobRodes Nov 08 '19 at 05:38