scaryguy scaryguy - 11 months ago 51
Ruby Question

How to create a custom implementation of inject method?

It looks pretty easy at first. Here is the first code block that I wrote in a few minutes:

module Enumerable

def my_inject(memo=false)
memo = memo ? memo : self[0]
self.my_each do |item|
memo = yield(memo, item)
return memo


When the inject method is called with an initial value, it works as expected. But I haven't been able to find a way to make it properly work without any initial value.

For example:

puts [1,2,3,4].my_inject(0) { |memo, i| memo+i} #=> 10
puts [1,2,3,4].my_inject { |memo, i| memo+i} #=> 11

And this is what original Ruby inject method outputs:

puts [1,2,3,4].inject { |memo, i| memo+i} #=> 10

The Ruby docs says

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

So how to set proper data type for initial value? Should we have a condition for every data type in Ruby and set a default value for each of data types?

Answer Source

If you debug Ruby's own implementation you will see that it starts the iteration from the second element in case no default memo is given:

> [1,2,3].inject { |m, i| puts "M: #{m} | I: #{i}"; break }
M: 1 | I: 2

Therefore your implementation should look like this:

def my_inject(*args)
  init = args.size > 0
  memo = init ? args[0] : self[0]

  self.drop(init ? 0 : 1).my_each do |item|
    memo = yield(memo, item)

  return memo

puts [1,2,3,4].my_inject(0) { |memo, i| memo+i} #=> 10
puts [1,2,3,4].my_inject { |memo, i| memo+i}    #=> 10

The reason for init = args.size > 0 is you have to take into account that someone might want to do [...].my_inject(false) { ... }. Therefore you cannot check if memo == false to determine if you need to skip the first element or not, you have to check the actual argument count that was given.