Maniaci
Maniaci

Reputation: 21

Can't shovel string into new array in Ruby

I am trying to search an array for a substring and if that substring exists, shovel it into a new array. The problem I am having is that it keeps coming back with this error message:

`block in substrings': undefined method `<<' for nil:NilClass

I have verified that the index in the method is not nil by printing it. I have also done index == nil to double check.

What am I missing here?

Thanks in advance for your help!

new_array = []

def substrings(word, array)

  new_array = array.each do |index|

    if index.include? (word)

      p index
      p index == nil

      new_array << index

    end
  end
end


dictionary = ["below", "down", "go", "going", "horn", "how", "howdy", "it", "i", "low", "own", "part", "partner", "sit"]

substrings("i", dictionary)

Upvotes: 1

Views: 239

Answers (3)

Stefan
Stefan

Reputation: 114208

I have verified that the index in the method is not nil by printing it. I have also done index == nil to double check.

What am I missing here?

Not index is nil, but new_array. The error message says:

undefined method `<<' for nil:NilClass

and refers to the line new_array << index. Here, << is the method and new_array is the receiver. And for some reason, new_array is nil.

You probably expected new_array to be [] because you explicitly said new_array = []. But methods have their own local variable scope. If you define a local variable outside of a method, it won't be available inside, or vice-versa.

Typically when referring to an undefined variable you'd get:

undefined local variable or method `new_array'

but here, the 2nd assignment conceals the actual problem:

  new_array = array.each do |index| 
  ^^^^^^^^^^^

When Ruby encounters this line, it immediately creates a local variable new_array with an initial value of nil. (local variables are created when the line is parsed, not when the assignment occurs)


To get the expected result, you have to move new_array = [] into the method, get rid of the new_array = array.each { ...} assignment and return new_array at the end:

def substrings(word, array)
  new_array = []

  array.each do |index|
    if index.include?(word)
      new_array << index
    end
  end

  new_array
end

The variable names are a bit arbitrary, maybe even misleading. Having index.include?(word) looks like you're comparing a numerical index to a string. I'd use something like this:

def substrings(substring, words)
  result = []

  words.each do |word|
    if word.include?(substring)
      result << word
    end
  end

  result
end

Code-wise, you can incorporate the array into the loop via each_with_object which will also return the array:

def substrings(substring, words)
  words.each_with_object([]) do |word, result|
    if word.include?(substring)
      result << word
    end
  end
end

However, selecting elements based on a condition is such a common task that Ruby provides a dedicated method select – you merely have to return true or false from the block to indicate whether the element should be selected:

def substrings(substring, words)
  words.select do |word|
    word.include?(substring)
  end
end

Upvotes: 0

user1934428
user1934428

Reputation: 22291

I would like to extend a bit the (correct) answer given by @DanneManne: While it is correct that you can access local variables from outer blocks from within an inner block, you can't do it within a def, and it is unnecessary in your example, because you can initialize new_array inside the body of your method and return it as result. But in case you ever really need this kind of construct, there indeed is a workaround:

So, this does NOT work:

a=5
def f
  puts a; # WRONG. a is not known here
end

and this works different than you seem to expect:

a=5
def f
  a=6
end

puts a # prints 5
f
puts a # prints 5 again

But if you define your method like this, it works:

a=5
define_method(:f) do
  puts a;  # OK, refers to outer variable a
end

By using a block, you create a closure with this, so if you do now a

f;
a=6;
f

5 and 6 is printed, in this order.

Upvotes: 0

DanneManne
DanneManne

Reputation: 21180

You basically combine two different ways of solving this problem. The first is to assign the new_array the result of looping though the array, but in that case, the new_array variable is not available to use inside the block.

So you could either choose to create the variable first, like this

new_array = []

array.each do |index| 
  if index.include?(word)
    new_array << index
  end
end

Alternatively you could use a method called reduce which takes a more functional programming approach. That could look like this

new_array = array.reduce([]) do |arr, index| 
  if index.include?(word)
    arr << index
  else
    arr
  end
end

What reduce does is that the block argument arr is always set to the return value of the previous block execution. that can make the syntax a little longer than it has to be, so Ruby also has an alternate approach to reduce, called each_with_object, that does the same, but by mutating the same variable, instead of requiring a return value. I actually prefer this way and would solve it like this.

new_array = array.each_with_object([]) do |index, arr| 
  arr << index if index.include?(word)
end

Upvotes: 1

Related Questions