Why Ruby Inject Method Cannot Sum Up String Lengths Without Initial Value

Why Ruby inject method cannot sum up string lengths without initial value?

You have the answer in apidock :

If you do not explicitly specify an initial value for memo, then uses the first element of collection is used as the initial value of memo.

That is, without an initial value, you're trying to do 'hello' + 'stack'.length

Why do I get a 'typeerror' when using inject in Ruby?

The result of results << sum + n is an array results and it's this that's replacing the sum value and so the next iteration you're trying to add a fixnum n into an array sum ... plus it doesn't help that you're initializing the value of sum to be an array.

Make sure that the last executed statement in your inject block is what you want the accumulated value to be.

def running_totals(myarray)
results = []
results << myarray.inject do |sum, n|
results << sum
sum + n
end
results
end

p running_totals([1,2,3,4,5])
=> [1, 3, 6, 10, 15]

Note that I moved the result of the inject into results array as well, so that the final value is also included, otherwise you'd only have the four values and would be missing the final (15) value.

Ruby: inject issue when turning array into hash

Just because Ruby is dynamically and implicitly typed doesn't mean that you don't have to think about types.

The type of Enumerable#inject without an explicit accumulator (this is usually called reduce) is something like

reduce :: [a] → (a → a → a) → a

or in a more Rubyish notation I just made up

Enumerable[A]#inject {|A, A| A } → A

You will notice that all the types are the same. The element type of the Enumerable, the two argument types of the block, the return type of the block and the return type of the overall method.

The type of Enumerable#inject with an explicit accumulator (this is usually called fold) is something like

fold :: [b] → a → (a → b → a) → a

or

Enumerable[B]#inject(A) {|A, B| A } → A

Here you see that the accumulator can have a different type than the element type of the collection.

These two rules generally get you through all Enumerable#inject-related type problems:

  1. the type of the accumulator and the return type of the block must be the same
  2. when not passing an explicit accumulator, the type of the accumulator is the same as the element type

In this case, it is Rule #1 that bites you. When you do something like

acc[key] = value

in your block, assignments evaluate to the assigned value, not the receiver of the assignment. You'll have to replace this with

acc.tap { acc[key] = value }

See also Why Ruby inject method cannot sum up string lengths without initial value?


BTW: you can use destructuring bind to make your code much more readable:

a.inject({}) {|r, (key, value)| r[key] = value; r }

Deeper explanation of reduce / inject method in ruby

I had a similar issue with the default values in Ruby inject/reduce methods, so I've tried to visualize it:

default values vizualized

Writing Ruby inject method?

As people have pointed out in comments, your solution double taps the first element if no argument is provided.

Here's an implementation that is pretty straightforward, drops some unnecessary elements your solution included, and works for more than just arrays:

module Enumerable
def my_inject(memo = nil)
each { |x| memo = memo.nil? ? x : yield(memo, x) }
memo
end
end

p (1..5).my_inject(&:*) # 5 factorial => 120
p (1..5).my_inject(2, &:*) # 5 factorial doubled => 240
p %w(3 4 5).my_inject(&:+) # string concatenation => "345"
p %w(3 4 5).my_inject("hello", &:+) # concatenation w/ prefix => "hello345"
p %w(3 4 5).my_inject("howdy") { |memo, x| memo + x } # prefix and block => "howdy345"

ADDENDUM

If you want to go further and handle Symbol or String arguments as Enumerable#inject does, you need to do some preprocessing to determine what you're dealing with:

module Enumerable
def my_inject(memo = nil, sym = nil, &block)
memo = memo.to_sym if memo.is_a?(String) && !sym && !block
block, memo = memo.to_proc, nil if memo.is_a?(Symbol) && !sym
sym = sym.to_sym if sym.is_a?(String)
block = sym.to_proc if sym.is_a?(Symbol)

# Ready to rock & roll
each { |x| memo = memo.nil? ? x : block.yield(memo, x) }
memo
end
end

# A variety of test cases
p (1..4).my_inject(:*) # 4 factorial via Symbol => 24
p (1..5).my_inject('*') # 5 factorial via String => 120
p (1..6).my_inject { |memo, x| memo * x } # 6 factorial via block => 720
p (1..5).my_inject(2, &:*) # 5 factorial doubled via Proc => 240
p (1..5).my_inject(3, :*) # 5 factorial tripled via Symbol => 360
p (1..5).my_inject(4, '*') # 5 factorial quadrupled via String => 480
p %w(3 4 5).my_inject(&:+) # string concatenation via Proc => "345"
p %w(3 4 5).my_inject("hello", &:+) # prefix and Proc => "hello345"
p %w(3 4 5).my_inject("howdy") { |memo, x| memo + x } # prefix and block => "howdy345"
p %w(3 4 5).my_inject("yowza", :+) # prefix and Symbol => "yowza345"
p %w(3 4 5).my_inject("yoiks", '+') # prefix and String => "yoiks345"

using inject to count elements in array

inject calls your block with two parameters, memo and current element. It then takes return value of the block and replaces memo with it. Your block returns integers. So, after first iteratation, your memo is not a hash anymore, it's an integer. And integers don't accept strings in their indexers.

Fix is easy, just return hash from the block.

a = ["the","the","a", "it", "it", "it"]
a.inject(Hash.new(0)) {|hash,word| hash[word] += 1; hash }

You may prefer each_with_object because it doesn't replace memo. Note that each_with_object accepts parameters in reverse (element first, memo second).

a.each_with_object(Hash.new(0)) {|word, hash| hash[word] += 1}

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

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.

TypeError: can't convert String into Integer Error while using inject

In this case, consider using group_by and count instead:

arr = [1,2,2,3]
throwaway_hash = arr.group_by{|x| x}
result_hash = Hash[throwaway_hash.map{|value, values| [value, values.count]}]
# => {1=>1, 2=>2, 3=>1}

Nil class when using Ruby injection

The expression count + 1 if (x%2 == 0) returns nil when the condition isn't true, which count gets set to because that's the nature of the inject method.

You could fix it by returning count + 1 when it's an even number and just count when it's not:

(1..10).inject(0) { |count,x| x % 2 == 0 ? count + 1 : count }

A completely different solution is to use select to select the even numbers and use the Array#length method to count them.

(1..10).select { |x| x % 2 == 0 }.length


Related Topics



Leave a reply



Submit