Recently I discovered that tap can be used in order to "drily" assign values to new variables; for example, for creating and filling an array, like this:
array = [].tap { |ary| ary << 5 if something }
This code will push 5 into array if something is truthy; otherwise, array will remain empty.
But I don't understand why after executing this code:
array = [].tap { |ary| ary += [5] if something }
array remains empty. Can anyone help me?
In the first case array and ary point to the same object. You then mutate that object using the << method. The object that both array and ary point to is now changed.
In the second case array and ary again both point to the same array. You now reassign the ary variable, so that ary now points to a new array. Reassigning ary however has no effect on array. In ruby reassigning a variable never effects other variables, even if they pointed to the same object before the reassignment.
In other words array is still empty for the same reason that x won't be 42 in the following example:
x = 23
y = x
y = 42 # Changes y, but not x
Edit: To append one array to another in-place you can use the concat method, which should also be faster than using +=.
I want to expand on this a bit:
array = [].tap { |ary| ary << 5 if something }
What this does (assuming something is true-ish):
assigns array to [], an empty array.
array.object_id = 2152428060
passes [] to the block as ary. ary and array are pointing to the same array object.
array.object_id = 2152428060
ary.object_id = 2152428060
ary << 5 << is a mutative method, meaning it will modify the receiving object. It is similar to the idiom of appending ! to a method call, meaning "modify this in place!", like in .map vs .map! (though the bang does not hold any intrinsic meaning on its own in a method name). ary has 5 inserted, so ary = array = [5]
array.object_id = 2152428060
ary.object_id = 2152428060
We end with array being equal to [5]
In the second example:
array = [].tap{ |ary| ary += [5] if something }
same
same
ary += 5 += is short for ary = ary + 5, so it is first modification (+) and then assignment (=), in that order. It gives the appearance of modifying an object in place, but it actually does not. It creates an entirely new object.
array.object_id = 2152428060
ary.object_id = 2152322420
So we end with array as the original object, an empty array with object_id=2152428060 , and ary, an array with one item containing 5 with object_id = 2152322420. Nothing happens to ary after this. It is uninvolved with the original assignment of array, that has already happened. Tap executes the block after array has been assigned.
Related
Given the hash
person = {
"cats"=> 2,
"dogs"=> 1
}
I wish to construct the array
["cats", "cats", "dogs"]
"cats" appears twice because person["cats"] #=> 2. For the same reason "dogs" appears once. If the hash had a third key-value pair "pigs"=>3, I would want to return the array
["cats", "cats", "dogs", "pigs", "pigs", "pigs"]
I tried the following code.
arr = person.to_a
i = 0
new_arr = []
while i < arr.length
el = arr[i][0]
final = [new_arr << el]
print final.flatten
i += 1
end
This displays
["cats"]["cats", "dogs"] => nil
but does not seem to return a value.
new_arr
#=> ["cats", "dogs"]
As you see, I am not getting the answer I wanted and do not understand why print displays what I show above.
I would like to know what is wrong with my code and what would be a better way of doing this.
flat_map method will flatten multiple arrays into one
Array operator * creates array with multiple values
result = person.flat_map {|key, value| [key] * value}
# => ["cats", "cats", "dogs"]
Ruby has a lot of nice methods to work with collections. I believe it is better to use them instead of while loop.
You can iterate through the hash using inject
method. The first parameter in the block is the resulting array, that accumulates the result of each iteration, the second is a key/value pair.
person.inject([]) do |array, (key, value)|
array + Array.new(value, key)
end
Or it can be rewritten as a one line.
person.inject([]) { |array, (key, value)| array + Array.new(value, key) }
I just started studying Ruby a little while ago and I was having difficulties with global versus local variable scoping.
Working on a practice problem, I found that an array defined globally was being changed by a function called on it. If I explicitly assign the array to something else, nothing changes. But if I run through and delete items one by one, this deletes them from the global array itself.
Why do delete and pop (which I also tested) methods have this behavior? I understood from reading that this should not be happening, that the "array" inside the functions is a reference to the values of arr, rather than the variable arr.
(I'm using Ruby version 2+)
def change_int x
x += 2
end
def change_arr array
array = [4, 5, 6]
end
def pop_arr array
puts array
new_array = []
while array.length > 0
new_array.push array[0]
array.delete_at 0
end
array
end
x = 5
change_int x
puts x == 5 # true
arr = [1, 2, 3]
change_arr arr
puts arr == [1, 2, 3] # true
old_arr = arr
puts pop_arr arr
puts arr == [1, 2, 3] # false
puts "arr = #{arr}" # arr = []
You can see by printing #object_id before calling pop_arr and inside pop_arr that those arrays are the same objects. This means that arguments are passed into the function by reference in Ruby.
Here is code:
def pop_arr(array)
puts array.object_id
# Rest of the fucntion
end
arr = [1, 2, 3]
puts arr.object_id
pop_arr(arr)
All of this means that when you edit array inside the function it will have effect on the object which was passed. #delete, #delete_at, #pop are operations that change the Array on which they are made.
See also: Ruby - Parameters by reference or by value? and Is Ruby pass by reference or by value?.
The curious thing is that change_arr doesn't affect the global array, but pop_arr does, in your code.
Here's what's happening: ruby passes references to objects as parameters. So like Bartosz said, you can see that at the top of those methods, the object id matches the one you passed in; they're referencing the same object.
So, in pop_arr, when you call delete_at, you're operating on the same object that you passed in, and the changes persist after the method returns.
In change_arr, the difference is that you're assigning the internal var to a new object. When you pass in the parameter array, the internal variable references the same object you passed in. When you instantiate a new Array object and assign the internal array variable to it, the internal variable is now referencing a different object.
def change_arr array
puts "change id: #{array.object_id}"
array = [4, 5, 6]
puts "change id2: #{array.object_id}"
array
end
That's why the changes don't persist after the method ends. If you wanted the changes to persist, you'd have to say
array = change_arr(array)
Hope that helps.
I have code like this:
combinedbooks = [[
"0000A|0000B",
"0000A|0000D",
"0000B|0000D"
]]
h = Hash[combinedbooks.map {|x| [x, 1]}]
The result is:
{["0000A|0000B", "0000A|0000D", "0000B|0000D"]=>1}
What I want to have is the following:
{["0000A|0000B"]=>1, ["0000A|0000D"]=>1, ["0000B|0000D"]=>1}
I cant figure out whats the problem, I believe that there is a problem with the array declaration but im not sure about it
If you want the keys to be single-item arrays, make them arrays:
arr = ["0000A|0000B", "0000A|0000D", "0000B|0000D"]
Hash[arr.map { |x| [[x], 1] }]
# => {["0000A|0000B"]=>1, ["0000A|0000D"]=>1, ["0000B|0000D"]=>1}
The format of your input has changed to a doubly-nested array. If that's accurate, simply use my solution, but map the first element of your array instead of the top-most array:
combinedbooks = [[ "0000A|0000B", "0000A|0000D", "0000B|0000D" ]]
Hash[combinedbooks[0].map { |x| [[x], 1] }]
# => {["0000A|0000B"]=>1, ["0000A|0000D"]=>1, ["0000B|0000D"]=>1}
Im not sure how to just push elements of one array into another without creating an array of arrays
There are two ways people usually append to an array, and it's important to understand the difference between them. Meditate on these:
Append/push an array to another array using << results in a sub-array:
foo = []
foo << [1]
foo # => [[1]]
Concatenate/Add an array to another array using += results in the elements of the second array being appended, not the array itself:
foo = []
foo += [1]
foo # => [1]
What is the best way to pass a method to reduce or inject instead of block like this:
def super_process(list, item)
list ||= []
list << another_method(item) + just_another_method
end
arr = ['1', '2', '3']
arr.reduce(&method(:super_process))
I have a problem with handling of list (it's default value). It's assigned to the first element of arr on first iteration but on next iteration it's assigned to the result of the first one.
I know I can write:
arr.reduce {|list, item| list << another_method(list, item) }
But that seems quite long and inexpressive to me.
The problem in your example is due to not passing the initial value to reduce. From ruby-doc.org:
reduce { |memo, obj| block } → obj
...
If you do not explicitly specify an initial value for memo, then the
first element of collection is used as the initial value of memo.
Therefore you probably want to pass an array as the first argument. I've changed the definition of super_process to something simpler:
def super_process list, item
list.push item + 1
end
arr = [1, 2, 3]
res = arr.reduce [], &method(:super_process)
puts res
This will output
2
3
4
Inspired by How can I marshal a hash with arrays? I wonder what's the reason that Array#<< won't work properly in the following code:
h = Hash.new{Array.new}
#=> {}
h[0]
#=> []
h[0] << 'a'
#=> ["a"]
h[0]
#=> [] # why?!
h[0] += ['a']
#=> ["a"]
h[0]
#=> ["a"] # as expected
Does it have to do with the fact that << changes the array in-place, while Array#+ creates a new instance?
If you create a Hash using the block form of Hash.new, the block gets executed every time you try to access an element which doesn't actually exist. So, let's just look at what happens:
h = Hash.new { [] }
h[0] << 'a'
The first thing that gets evaluated here, is the expression
h[0]
What happens when it gets evaluated? Well, the block gets run:
[]
That's not very exciting: the block simply creates an empty array and returns it. It doesn't do anything else. In particular, it doesn't change h in any way: h is still empty.
Next, the message << with one argument 'a' gets sent to the result of h[0] which is the result of the block, which is simply an empty array:
[] << 'a'
What does this do? It adds the element 'a' to an empty array, but since the array doesn't actually get assigned to any variable, it is immediately garbage collected and goes away.
Now, if you evaluate h[0] again:
h[0] # => []
h is still empty, since nothing ever got assigned to it, therefore the key 0 is still non-existent, which means the block gets run again, which means it again returns an empty array (but note that it is a completely new, different empty array now).
h[0] += ['a']
What happens here? First, the operator assign gets desugared to
h[0] = h[0] + ['a']
Now, the h[0] on the right side gets evaluated. And what does it return? We already went over this: h[0] doesn't exist, therefore the block gets run, the block returns an empty array. Again, this is a completely new, third empty array now. This empty array gets sent the message + with the argument ['a'], which causes it to return yet another new array which is the array ['a']. This array then gets assigned to h[0].
Lastly, at this point:
h[0] # => ['a']
Now you have finally actually put something into h[0] so, obviously, you get out what you put in.
So, to answer the question you probably had, why don't you get out what you put in? You didn't put anything in in the first place!
If you actually want to assign to the hash inside the block, you have to, well assign to the hash inside the block:
h = Hash.new {|this_hash, nonexistent_key| this_hash[nonexistent_key] = [] }
h[0] << 'a'
h[0] # => ['a']
It's actually fairly easy to see what is going on in your code example, if you look at the identities of the objects involved. Then you can see that everytime you call h[0], you get a different array.
The problem in your code is that h[0] << 'a' makes an new Array and gives it out when you index with h[0], but doesn't store the modified Array anywhere after the << 'a' because there is no assignment.
Meanwhile h[0] += ['a'] works because it's equivalent to h[0] = h[0] + ['a']. It's the assignment ([]=) that makes the difference.
The first case may seem confusing, but it is useful when you just want to receive some unchanging default element from a Hash when the key is not found. Otherwise you could end up populating the Hash with a great number of unused values just by indexing it.
h = Hash.new{ |a,b| a[b] = Array.new }
h[0] << "hello world"
#=> ["hello world"]
h[0]
#=> ["hello world"]