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