2

So, I'm going through The Odin Project's Ruby Path, and in one of the Projects we have to rewrite some of the Enum functions to their basic functionality. Thing is, I want to try to go beyond and recreate those functions as close to the original ones as possible, which brings me to Enum#inject.

I have recreated it using the following code (which is inside the Enumerable module)

def my_inject(initial = first, sym = nil)
  memo = initial

  enum = to_a

  if block_given?
    enum.my_each_with_index do |el, idx|
      next if memo == first && idx.zero?

      memo = yield memo, el
    end
  else
    block = sym.to_proc
    enum.my_each_with_index do |el, idx|
      next if memo == first && idx.zero?

      memo = block.call memo, el
    end
  end
  memo
end

(my_each_with_index being a custom version of each_with_index that should work like so)

This version is almost working fine. Problem is only when I call it with only a Symbol as argument, like in ((5..10).my_inject(:+)), cause it throws a 'my_inject': undefined method 'to_proc' for nil:NilClass (NoMethodError).

I'm guessing this is happening because the symbol is being passed as the initial value, as the first parameter of the function.

I thought about trying to write a bunch of checks (like to check if the function has only one argument and that argument is a symbol), but I wanna know if there is an easier and cleaner way of doing it so.

(Please, bear in mind I've been studying code for no more than 6 months, so I am a very very VERY green at this).

I appreciate the help!

jampamatos
  • 37
  • 5

1 Answers1

2

The build-in inject is quite polymorphic, so before trying to implement it from scratch (without looking at the source code) we would need to explore how it behaves in different cases. I skip things that you already know (like using the 1st element as an initial value if not provided explicitly etc), other than that:

[1,2,3].inject(0, :+, :foo) #=> ArgumentError: wrong number of arguments (given 3, expected 0..2)
# Max. arity is strict

[1,2,3].inject(0, :+) { 1 } #=> 6
# If both symbol _and_ block are provided, the former dominates

[1,2,3].inject(:+) { |acc, x| acc } #=> :+
# With only 1 parameter and a block the former will be treated as an init value, not a proc.

[1,2,3].inject("+") #=> 6
[1,2,3].inject("+") { |acc, x| acc } #=> "+"
# Strings work too. This is important, because strings _don't respond to `to_proc`_, so we would need smth. else

[1,2,3].inject #=> LocalJumpError: no block given
# Ok, local jump error means that we try to yield in the cases where the 2nd parameter is not provided

[1,2,3].inject(nil) #=> TypeError: nil is not a symbol nor a string
# But if it is provided, we try to do with it something that quacks pretty much like `send`...

With these bits in mind, we can write something like

module Enum
  # Just for convenience, you don't need it if you implement your own `each`
  include Enumerable

  def my_inject(acc = nil, sym = nil)
    # With a single attribute and no block we assume that the init value is in fact nil
    # and what is provided should be "called" on elements somehow
    if acc && !sym && !block_given?
      sym, acc = acc, nil
    end
  
    each do |element|
      if !acc
        # If we are not initialized yet, we just assign an element to our accumulator
        # and proceed
        acc = element
      elsif sym
        # If the "symbol" was provided explicitly (or resolved as such in a single parameter case)
        # we try to call the appropriate method on the accumulator. 
        acc = acc.send(sym, element)
      else
        # Otherwise just try to yield
        acc = yield acc, element
      end
    end
    
    acc
  end
end

Bear with me, we're almost there :) Just let's check how it quacks:

class Ary < Array
  include Enum
end

ary = Ary.new.push(1,2,3)

ary.my_inject #=> LocalJumpError: no block given
ary.my_inject(0) #=> TypeError: 0 is not a symbol nor a string
ary.my_inject("+") #=> 6
ary.my_inject(:+) #=> 6
ary.my_inject(0, :+) #=> 6
ary.my_inject(1, :+) #=> 7
ary.my_inject(1, :+) { 1 } #=> 7
ary.my_inject(:+) { |acc, x| acc } #=> :+

So, pretty much the same. There might be some other edge cases that my implementation doesn't satisfy, but I leave them to you :)

Konstantin Strukov
  • 2,899
  • 1
  • 10
  • 14
  • It took me a little while to understand it, but once I did, I really loved it! I though about doing something like what you did in the first line of the method, but I imagined something much more complex and hard, this is beautiful. Also, I have never seen the `send` method, but I suppose it calls the symbol as a function and the other argument as a parameter? – jampamatos Aug 04 '22 at 17:54
  • `inject` is one of the most heavily overloaded methods in Ruby. However, Ruby does not allow (argument-based) overloading. Almost all Ruby implementations cheat when implementing `inject`. E.g. JRuby, IronRuby, Ruby.NET, and XRuby all implement `inject` in a language that does support argument-based overloading (Java and C#) as different overloads. Rubinius, YARV, MRI, and MRuby use languages that don't support overloading (Ruby, C), but they use internal information inside of the VM to figure out the signature of the message send. – Jörg W Mittag Aug 04 '22 at 18:35
  • Neither of those two tricks are available if you implement the method in pure Ruby, and then you end up with a giant mess of spaghetti code just to figure out which "overload" you are actually working with, none of which has anything to do with the actual `inject` algorithm. – Jörg W Mittag Aug 04 '22 at 18:36
  • 1
    @JörgWMittag sure, but it seems obvious from the question that we deal with an educational task (about Ruby basics, not internals). So "we cannot implement inject properly in Ruby" doesn't help that much. – Konstantin Strukov Aug 05 '22 at 12:23