Home > OS >  Rewriting Enum#inject to take a Symbol as a Parameter in Ruby
Rewriting Enum#inject to take a Symbol as a Parameter in Ruby

Time:08-05

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!

CodePudding user response:

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 :)

  • Related