Search code examples
arraysrubyhashtable

Why does the str.next! in an Enumerable#map fill the array with same elements?


Enumerable#map creates an array with the return values in the block after it's yielded.

In such case, say:

v = 'a'
26.times.map { |i| v.ord.+(i).chr }

# => ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

But why does the following codes fills the array with same elements?

v = '`'
26.times.map { v.next! }

# => ["z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z"]
v = '`'
Array.new(26) { v.next! }

# => ["z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z", "z"]

Shouldn't they all have elements a to z?

Again, this works:

v = '`'
Array.new(26) { v = v.succ }

# => ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

Actually I am trying to do:

v = "\xf0\x9d\x93\xa9"
('a'..'z').reduce({}) { |h, i| h.merge(i.intern => v = v.succ) }

# => {:a=>"𝓪", :b=>"𝓫", :c=>"𝓬", :d=>"𝓭", :e=>"𝓮", :f=>"𝓯", :g=>"𝓰", :h=>"𝓱", :i=>"𝓲", :j=>"𝓳", :k=>"𝓴", :l=>"𝓵", :m=>"𝓶", :n=>"𝓷", :o=>"𝓸", :p=>"𝓹", :q=>"𝓺", :r=>"𝓻", :s=>"𝓼", :t=>"𝓽", :u=>"𝓾", :v=>"𝓿", :w=>"𝔀", :x=>"𝔁", :y=>"𝔂", :z=>"𝔃"}

But I get all z's instead while using succ! / next!

v = "\xf0\x9d\x93\xa9"
('a'..'z').reduce({}) { |h, i| h.merge(i.intern => v.succ!) }

# => {:a=>"𝔃", :b=>"𝔃", :c=>"𝔃", :d=>"𝔃", :e=>"𝔃", :f=>"𝔃", :g=>"𝔃", :h=>"𝔃", :i=>"𝔃", :j=>"𝔃", :k=>"𝔃", :l=>"𝔃", :m=>"𝔃", :n=>"𝔃", :o=>"𝔃", :p=>"𝔃", :q=>"𝔃", :r=>"𝔃", :s=>"𝔃", :t=>"𝔃", :u=>"𝔃", :v=>"𝔃", :w=>"𝔃", :x=>"𝔃", :y=>"𝔃", :z=>"𝔃"}

Except succ! / next! doesn't change the memory location and object_id, aren't v.succ! and v = v.succ same?


Solution

  • When you call next! or succ! on a variable str, object assigned to this variable is mutated and a reference to this object is returned. If str = 'a' and you call str.next! 26 times, str becomes z. Every time next! is called, a reference to the same object is returned. As a result, you get an array of 26 references to the same object. That's why all of the elements in the array are the same.

    You can test that by checking object_id of array elements:

    pry(main)> str = 'a'
    'a'
    pry(main)> array = 3.times.map{ str.next!}
    => ["d", "d", "d"]
    pry(main)> array.map(&:object_id)
    => [47056742362940, 47056742362940, 47056742362940]
    pry(main)> array.map(&:object_id).uniq
    => [47056742362940]
    

    When you edit str, all array elements are updated:

    [39] pry(main)> str << "b"
    => "db"
    [40] pry(main)> array
    => ["db", "db", "db"]
    [41] pry(main)> str.replace
    str.replace
    [41] pry(main)> str.replace('a')
    => "a"
    [42] pry(main)> array
    => ["a", "a", "a"]
    

    If you want to have an array with the whole alphabet, you need to copy the string after changing current letter, see below:

    [25] pry(main)> str = 'a'
    => "a"
    [26] pry(main)> 25.times.map{ str.next!.dup} 
    => ["b",
     "c",
     "d",
     "e",
     "f",
     "g",
     "h",
     "i",
     "j",
     "k",
     "l",
     "m",
     "n",
     "o",
     "p",
     "q",
     "r",
     "s",
     "t",
     "u",
     "w",
     "x",
     "y",
     "z"]
    

    You can also use a range:

    [32] pry(main)> ('a'..'z').to_a
    => ["a",
     "b",
     "c",
     "d",
     "e",
     "f",
    ...