Use `next` with an accumulator argument in a `reduce` - ruby

There is a cop: RuboCop::Cop::Lint::NextWithoutAccumulator.
Is anyone able to explain what this cop is for, how is it supposed to improve the code in in what way?
Does it improve readability, efficiency?
github code.

Lets consider sample code from the documentation:
# bad
result = (1..4).reduce(0) do |acc, i|
next if i.odd?
acc + i
end
If you try this in the console, you will get NoMethodError exception for the nil object. This is because next "returns" nil if there is no object specified. You can treat it as return for iterators.
For reduce method it may result in some unexpected behavior as it needs some value returned by the block. If i is odd, then next is evaluated and block gives nil as the result. In the following iterator acc is equal to nil and it can't add integer to it. In our example, first iteration is for i = 1, next sets acc to nil as the result of the block.
In some cases you can get correct value for the enumerable, but in general it is safer to specify value for next inside.

Bad
result = (1..4).reduce(0) do |acc, i|
next if i.odd?
acc + i
end
Good
result = (1..4).reduce(0) do |acc, i|
next acc if i.odd?
acc + i
end
As #smefju pointed out, next by itself implicitly returns nil and will cause a NoMethodError when it is passed in as the parameter of the next execution of the block.

Related

How to use reduce/inject in Ruby without getting Undefined variable

When using an accumulator, does the accumulator exist only within the reduce block or does it exist within the function?
I have a method that looks like:
def my_useless_function(str)
crazy_letters = ['a','s','d','f','g','h']
str.split.reduce([]) do |new_array, letter|
for a in 0..crazy_letters.length-1
if letter == crazy_letters[a]
new_array << letter
end
end
end
return true if (new_array == new_array.sort)
end
When I execute this code I get the error
"undefined variable new_array in line 11 (the return statement)"
I also tried assigning the new_array value to another variable as an else statement inside my reduce block but that gave me the same results.
Can someone explain to me why this is happening?
The problem is that new_array is created during the call to reduce, and then the reference is lost afterwards. Local variables in Ruby are scoped to the block they are in. The array can be returned from reduce in your case, so you could use it there. However, you need to fix a couple things:
str.split does not break a string into characters in Ruby 2+. You should use str.chars, or str.split('').
The object retained for each new iteration of reduce must be retained by returning it from the block each time. The simplest way to do this is to put new_array as the last expression in your block.
Thus:
def my_useless_function(str)
crazy_letters = ['a','s','d','f','g','h']
crazy_only = str.split('').reduce([]) do |new_array, letter|
for a in 0..crazy_letters.length-1
if letter == crazy_letters[a]
new_array << letter
end
end
new_array
end
return true if (crazy_only == crazy_only.sort)
end
Note that your function is not very efficient, and not very idiomatic. Here's a shorter version of the function that is more idiomatic, but not much more efficient:
def my_useless_function(str)
crazy_letters = %w[a s d f g h]
crazy_only = str.chars.select{ |c| crazy_letters.include?(c) }
crazy_only == crazy_only.sort # evaluates to true or false
end
And here's a version that's more efficient:
def efficient_useless(str)
crazy_only = str.scan(/[asdfgh]/) # use regex to search for the letters you want
crazy_only == crazy_only.sort
end
Block local variables
new_array doesn't exist outside the block of your reduce call. It's a "block local variable".
reduce does return an object, though, and you should use it inside your method.
sum = [1, 2, 3].reduce(0){ |acc, elem| acc + elem }
puts sum
# 6
puts acc
# undefined local variable or method `acc' for main:Object (NameError)
Your code
Here's the least amount of change for your method :
def my_useless_function(str)
crazy_letters = ['a','s','d','f','g','h']
new_array = str.split(//).reduce([]) do |new_array, letter|
for a in 0..crazy_letters.length-1
if letter == crazy_letters[a]
new_array << letter
end
end
new_array
end
return true if (new_array == new_array.sort)
end
Notes:
return isn't needed at the end.
true if ... isn't needed either
for loop should never be used in Ruby
reduce returns the result of the last expression inside the block. It was for in your code.
If you always need to return the same object in reduce, it might be a sign you could use each_with_object.
"test".split is just ["test"]
String and Enumerable have methods that could help you. Using them, you could write a much cleaner and more efficient method, as in #Phrogz answer.

Why isn't my print_linked_list_in_reverse function working?

One challenge in a Ruby course I'm doing is to print the :data values of the following linked list, in reverse:
{:data=>3, :next=>{:data=>2, :next=>{:data=>1, :next=>nil}}}
So when my method is passed the above code, it should return
1
2
3
Here's my attempt, which doesn't work for the above code. I can't figure out why, and I'd appreciate it if someone could explain what I'm doing wrong:
def print_list_in_reverse(hash)
if hash[:next].nil? #i.e. is this the final list element?
print "#{hash[:data]}\n"
return true
else
#as I understand it, the next line should run the method on `hash[:next]` as well as checking if it returns true.
print "#{hash[:data]}\n" if print_list_in_reverse(hash[:next])
end
end
Here's a solution, in case it helps you spot my mistake.
def print_list_in_reverse(list)
return unless list
print_list_in_reverse list[:next]
puts list[:data]
end
Thank you.
Your solution relies on return values, and you don't explicitly provide one in your else clause. In fact, you implicitly do because Ruby returns the result of the last statement evaluated, which for a print statement is nil. In Ruby false and nil are both logically false, causing the print to get bypassed for all but the last two calls. Your choices are to add a true at the end of the else, or make a solution that doesn't rely on return values.
To negate the need for return values, just check what logic is kosher based on info in the current invocation. You can simplify your life by leveraging the "truthiness" non-nil objects. Your basic recursive logic to get things in reverse is "print the stuff from the rest of my list, then print my stuff." A straightforward implementation based on truthiness would be:
def print_list_in_reverse(hash)
print_list_in_reverse(hash[:next]) if hash[:next]
print "#{hash[:data]}\n"
end
The problem with that is that you might have been handed an empty list, in which case you don't want to print anything. That's easy to check:
def print_list_in_reverse(hash)
print_list_in_reverse(hash[:next]) if hash[:next]
print "#{hash[:data]}\n" if hash
end
That will work as long as you get handed a hash, even if it's empty. If you're paranoid about being handed a nil:
def print_list_in_reverse(hash)
print_list_in_reverse(hash[:next]) if hash && hash[:next]
print "#{hash[:data]}\n" if hash
end
The other alternative is to start by checking if the current list element is nil and returning immediately in that case. Otherwise, follow the basic recursive logic outlined above. That results in the solution you provided.
Better to iterate over every value in your hash, and push the values until there's no any other hash as value inside the main hash.
def print_list_in_reverse(hash, results = [])
hash.each_value do |value|
if value.is_a? Hash
print_list_in_reverse(value, results)
else
results << value unless value.nil?
end
end
results.reverse
end
p print_list_in_reverse(data)
=> [1, 2, 3]
The problem in your code is in the else-case. You need to return true to print the hash[:data].
Your method always print the last 2 elements.

How to break from a nested loop to a parent loop that is more than one level above which requires a value provided by the nested loop

In the following situation:
xxx.delete_if do |x|
yyy.descend do |y| # This is a pathname.descend
zzz.each do |z|
if x + y == z
# Do something
# Break all nested loops returning to "xxx.delete_if do |x|" loop
# The "xxx.delete_if do |x|" must receive a "true" so that it
# can delete the array item
end
end
end
end
What is the best way to achieve this multiple nested break while making sure I can pass the true value so that the array item is deleted?
Maybe I should use multiple break statements that return true or use a throw/catch with a variable, but I don't know if those are the best answer.
This question is different from How to break from nested loops in Ruby? because it requires that the parent loop receives a value from the nested loop.
throw/catch (NOT raise/rescue) is the way I typically see this done.
xxx.delete_if do |x|
catch(:done) do
yyy.each do |y|
zzz.each do |z|
if x + y == z
# Do something
throw(:done, true)
end
end
end
false
end
end
In fact, the Pickaxe explicitly recommends it:
While the exception mechanism of raise and rescue is great for abandoning execution when things go wrong, it's sometimes nice to be able to jump out of some deeply nested construct during normal processing. This is where catch and throw come in handy. When Ruby encounters a throw, it zips back up the call stack looking for a catch block with a matching symbol. When it finds it, Ruby unwinds the stack to that point and terminates the block. If the throw is called with the optional second parameter, that value is returned as the value of the catch.
That said, max's #any? suggestion is a better fit for this problem.
You can use multiple break statements.
For example:
xxx.delete_if do |x|
result = yyy.each do |y|
result2 = zzz.each do |z|
if x + y == z
break true
end
end
break true if result2 == true
end
result == true
end
However I would definitely avoid this in your particular situation.
You shouldn't be assigning variables to the result of each. Use map, reduce, select, reject, any?, all?, etc. instead
It makes more sense to use any? to accomplish the same purpose:
xxx.delete_if do |x|
yyy.any? do |y|
zzz.any? do |z|
x + y == z
end
end
end

Is there an implicit keyword in this Ruby Array map code?

Is there a keyword I can use to explicitly tell the map function what the result of that particular iteration should be?
Consider:
a = [1,2,3,4,5]
a.map do |element|
element.to_s
end
In the above example element.to_s is implicitly the result of each iteration.
There are some situations where I don't want to rely on using the last executed line as the result, I would prefer to explicitly say what the result is in code.
For example,
a = [1,2,3,4,5]
a.map do |element|
if some_condition
element.to_s
else
element.to_f
end
end
Might be easier for me to read if it was written like:
a = [1,2,3,4,5]
a.map do |element|
if some_condition
result_is element.to_s
else
result_is element.to_f
end
end
So is there a keyword I can use in place of result_is?
return will return from the calling function, and break will stop the iteration early, so neither of those is what I'm looking for.
The last thing left on the stack is automatically the result of a block being called. You're correct that return would not have the desired effect here, but overlook another possibility: Declaring a separate function to evaluate the entries.
For example, a reworking of your code:
def function(element)
if (some_condition)
return element.to_s
end
element.to_f
end
a.map do |element|
function(element)
end
There is a nominal amount of overhead on calling the function, but on small lists it should not be an issue. If this is highly performance sensitive, you will want to do it the hard way.
Yes, there is, it's called next. However, using next in this particular case will not improve readability. On the contrary, it will a) confuse the reader and b) give him the impression that the author of that code doesn't understand Ruby.
The fact that everything is an expression in Ruby (there are no statements) and that every expression evaluates to the value of the last sub-expression in that expression are fundamental Ruby knowledge.
Just like return, next should only be used when you want to "return" from the middle of a block. Usually, you only use it as a guard clause.
The nature of map is to assign the last executed line to the array. Your last example is very similar to the following, which follows the expected behavior:
a = [1,2,3,4,5]
a.map do |element|
result = if some_condition
element.to_s
else
element.to_f
end
result
end
No, there is no language keyword in ruby you can use to determine the result mapped into the resulting array before executing other code within the iteration.
You may assign a variable which you then return when some other code has been executed:
a.map do |element|
result = some_condition ? element.to_s : element.to_f
#do something else with element
result
end
Keep in mind the reason for ruby not providing a keyword for this kind of code is that these patterns tend to have a really low readability.

Ruby block method help

I was trying to see if I could reconstruct the Array class' delete_if iterator as my own method in order to see if I understood methods and blocks correctly. Here is what I coded:
def delete_if(arr)
for x in 0...arr.length
if (yield arr[x])
arr[x]=arr[x+1,arr.length]
redo
end
end
end
arr = [0,1,2,3,4,5]
delete_if(arr) {|value| value % 2 == 0}
This resulted in an error saying that the % method could not be identified in the last line. I know that value is going to be an integer so I am not sure why it would say this error. Can someone please explain? Also, in Ruby in general, how can you be sure that someone passes the correct type into a method? What if the method is supposed to take a string but they pass in an integer -- how do you prevent that??
Thanks!
def delete_if arr
for x in 0...arr.length
return if x >= arr.length
if yield arr[x]
arr[x..-1] = arr[(x + 1)..-1]
redo
end
end
end
Things I fixed:
it's necessary to mutate the array, if all you do is assign to the parameter, your changes will be local to the method. And for that matter, you were assigning your calculated array object to an element of the original array, which was the immediate cause of the error message.
since the array may become shorter, we need to bail out at the (new) end
of course you could just use arr.delete_at x but I couldn't correct the slice assignment without keeping the code pattern

Resources