collenjones
collenjones

Reputation: 530

Ruby .inject() -- need help understanding this code

This code takes an array and returns only unique values.

Why is the second 'keep' required for this code to work?? Without it, I get this error:

NoMethodError: undefined method `include?' for nil:NilClass

class Array
  def my_uniq_inject
    self.inject([]) do |keep, num|
      keep << num unless keep.include?(num)
      keep  # why is this required?
    end
  end
end

Upvotes: 0

Views: 156

Answers (6)

Neil Slater
Neil Slater

Reputation: 27207

inject feeds the output of the block back in at each stage, and returns the output of the block at the end.

When you are using inject to aggregate data, you need to return the object that is receiving the combined data at the end the block, otherwise on the next iteration the block variable will end up pointing at something else, causing odd effects - usually an error.

You don't have to use inject purely for aggregation though, and could in theory switch objects around if it suited your purpose. In practice I think this is a rare use of inject

Upvotes: 0

Dan Tao
Dan Tao

Reputation: 128317

You may be confused because normally arr << x returns arr, so you would think you're fine.

It's the unless part that can screw things up here. If the last element in the array is not unique (i.e., it's already appeared earlier in the array) then the unless clause will cause the expression to evaluate to nil.

See for yourself:

arr = []
arr << 1              # [1]
arr << 2 unless false # [1, 2]
arr << 3 unless true  # nil

Upvotes: 5

Jacob Brown
Jacob Brown

Reputation: 7561

The second keep is required because of your condition in the previous line. If keep.include?(num) evaluates to false, nil will be returned to the inject accumulator. That's not really what you want to happen: you basically want that iteration to be skipped, but retain the previous array. The second keep allows you to pass the array back to the accumulator.

Upvotes: 0

Mark Rushakoff
Mark Rushakoff

Reputation: 258138

You need keep at the end of the block, because the result of the block is used as the accumulator for the next iteration of inject.

Without that keep, the first line in your block will sometimes return keep but sometimes return nil (when the conditional is not met, specifically).

Upvotes: 1

Sergio Tulentsev
Sergio Tulentsev

Reputation: 230286

Because inject/reduce takes return value of your block and replaces memo/accumulator with it.

You could use each_with_object which doesn't replace the memo

self.each_with_object([]) do |num, keep|
  keep << num unless keep.include?(num)
end

Upvotes: 2

Arup Rakshit
Arup Rakshit

Reputation: 118261

The second keep is for to reinitialize the keep here do |keep,num|,after the first pass. See the doc enum#inject is saying at the second para last line At the end of the iteration, the final value of memo is the return value for the method.

Upvotes: 1

Related Questions