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:
- the type of the accumulator and the return type of the block must be the same
- 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:
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 eitherfor
loop should never be used in Rubyreduce
returns the result of the last expression inside the block. It wasfor
in your code.- If you always need to return the same object in
reduce
, it might be a sign you could useeach_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
Keyword Arguments Unpacking (Splat) in Ruby
Encoding::Undefinedconversionerror: "\Xe4" from Ascii-8Bit to Utf-8
Integrating @Font-Face Files into Rails Asset Pipeline
How to Convert Any Method to Infix Operator in Ruby
Why Does a Rails App on Heroku Serve Assets via All.CSS and Locally via Individual Files
Undefined Local Variable Based on Syntax in Ruby
How to Rescue from a 'Require': No Such File to Load in Ruby
How to Upload a Text File and Parse Contents into Database in Ror
Bundler - Load Multiple Gemfiles
Rails Fastercsv "Unquoted Fields Do Not Allow \R or \N"
How to Access Cookies from Applicationcontroller (Rails)