Reputation: 410
I'm going through this annoying tutorial on Rubymonk and it asks me to do the following:
Write for me three methods - calculate, add and subtract. The tests should all pass. Take a look at the hint if you have trouble! And as a little extra hint: remember that you can use something.is_a?(Hash) or another_thing.is_a?(String) to check an object's type.
I couldn't even understand what they ask me to do, so I just decided to see the solution and and work my way into a decent understanding of the task.
Here is the solution:
def add(*numbers)
numbers.inject(0) { |sum, number| sum + number }
end
def subtract(*numbers)
current_result = numbers.shift
numbers.inject(current_result) { |current_result, number| current_result - number }
end
def calculate(*arguments)
# if the last argument is a Hash, extract it
# otherwise create an empty Hash
options = arguments[-1].is_a?(Hash) ? arguments.pop : {}
options[:add] = true if options.empty?
return add(*arguments) if options[:add]
return subtract(*arguments) if options[:subtract]
end
I don't understand many things, but the one thing that baffles me is the shift method: current_result = numbers.shift. Why is it there? I mean, I understand what it does, but what's its job in this particular piece of code?
Btw, if someone goes to the trouble to break this code down for me I would be endlessly and eternally thankful.
The task is at the bottom of the following page: https://rubymonk.com/learning/books/1-ruby-primer/chapters/19-ruby-methods/lessons/69-new-lesson#solution3899
Upvotes: 1
Views: 341
Reputation: 18762
Explanation of subtract
method
Lets say your input was [2,3,4,5].
In your mind, you can think that you need a program which does this
result = 2 # This is first element of array
result = result - 3 # This is second element of array
result = result - 4 # This is third element of array
result = result - 5 # This is fourth element of array
So that finally, you end up with
result = -10
shift
allows you to take the first element from the array, so that you can use inject
on remaining elements with first element shifted of array acting as accumulator
value.
The above explanation will make sense if you are aware of how inject
method works.
Same approach could have been taken for add
method as well.
def add(*numbers)
current_result = numbers.shift
numbers.inject(current_result) { |current_result, number| current_result + number }
end
However, the authors of the add
code used the more orthodox version of inject
in their implementation as adding 0
to another number will not have any impact.
If authors used their add
method and replaced +
with -
, they would have ended up with
def subtract(*numbers)
numbers.inject(0) { |result, number| result - number }
end
However, subtract([2,3,4,5])
would have provided output of -14
(0-2-3-4-5 = -14
) instead of -10
(2-3-4-5 = -10
).
To fix the above code, one will have to get rid of default parameter 0
in both add
and subtract
, and write something like below
def subtract(*numbers)
numbers.inject { |result, number| result - number }
end
def add(*numbers)
numbers.inject { |result, number| result + number }
end
We will now get right result of -10
for subtract([2,3,4,5])
The above code works because as documented here (and also expressed in other answers)
The argument to inject is actually optional. If a default value is not passed in as an argument, the first time the block executes, the first argument will be set to the first element of the enumerable and the second argument will be set to the second element of the enumerable.
In summary, authors of Rubymonk could have avoided the confusion, or may be they were trying to teach the reader one more Ruby trick by using shift
Explanation of calculate
method
An easy to understand version of calculate
can be something like below:
def calculate(*arguments, options)
return add(*arguments) if options[:add]
return subtract(*arguments) if options[:subtract]
end
p calculate(1,2,3,4,{add: true}) # Adds the numbers, outputs 10
p calculate(1,2,3,4,{subtract: true}) # Subtracts the numbers, outputs -8
Ruby will assign last parameter of function call to options
and rest of elements as an array to arguments
. If we called calculate
with only one parameter, that will be the value of options
.
p calculate({add: true}) # Outputs 0
p calculate(1) # Will return error as 1 is not a Hash and can't be assigned to 'options'
Author of code wanted to make options
an optional argument, so that below code worked as well.
p calculate(1,2,3,4) # Assume add, and output 10
That will require a change in the simpler version of calculate
such that instead of explicitly declaring options
as the argument to calculate
, we can use the last element of arguments
array as options
if it is of type Hash
. We will thus end up implementing the calculate
as shown on the Rubymonk.
Upvotes: 2
Reputation: 110685
add(*numbers)
Let's start by invoking:
def add(*numbers)
numbers.inject(0) { |sum, number| sum + number }
end
like this:
add(1,2,3) #=> 6
or like this:
add(*[1,2,3]) #=> 6
The two are equivalent. The latter shows you what the operator "splat" does.
This results in:
numbers #=> [1,2,3]
so Ruby sends Enumerable#inject (aka reduce
) to numbers
:
[1,2,3].inject(0) { |sum, number| sum + number }
inject
first initializes the "memo" sum
to inject
's argument (if, as here, it has one) and then passes the first element of the "receiver" [1,2,3]
into the block and assigns it to the block variable number
:
sum #=> 0
number #=> 1
Ruby then computes:
sum + number #=> 0 + 1 => 1
which becomes the new value of the memo. Next, inject
passes 2
into the block and computes:
sum #=> 1
number #=> 2
sum + number #=> 3
so (the memo) sum
is now 3
.
Lastly,
sum #=> 3
number #=> 3
sum + number #=> 6
As all elements of the receiver have been passed into the block, inject
returns the value of the memo:
sum #=> 6
If you examine the doc for inject
you'll see that if the method has no argument Ruby assigns the first element of the receiver (here 1
) to the memo (sum
) and then carries on as above starting with the second element of the receiver (2
). As expected, this produces the same answer:
def add(*numbers)
numbers.inject { |sum, number| sum + number }
end
add(1,2,3) #=> 6
So why include the argument zero? Often we will want add()
(i.e., add(*[]))
to return zero. I will leave it to you to investigate what happens here with each of the two forms of inject
. What conclusion can you draw?
As @Stefan points out in his answer, you can simply this to:
def add(*numbers)
numbers.inject :+
end
which is how you'd normally see it written.
If, however, numbers
may be an empty array, you'd want to provide an initial value of zero for the memo:
def add(*numbers)
numbers.inject 0, :+
end
add(*[]) #=> 0
subtract(*numbers)
def subtract(*numbers)
current_result = numbers.shift
numbers.inject(current_result) { |current_result, number|
current_result - number }
end
This is similar to the method add
, with a small twist. We need the first value of the memo (here current_result
) to be the first element of the receiver. There are two ways we could do that.
The first way is like this:
def subtract(*numbers)
numbers[1..-1].inject(numbers.first) { |current_result, number|
current_result - number }
end
numbers = [6,2,3]
subtract(*numbers) #=> 1
Let
first_number = numbers.first #=> 6
all_but_first = numbers[1..-1] #=> [2,3]
then:
numbers[1..-1].inject(numbers.first) { ... }
is:
all_but_first.inject(first_number) { ... }
#=> [2,3].inject(6) { ... }
Instead, the author chose to write:
first_number = numbers.shift #=> 6
numbers #=> [2,3]
numbers.inject(first_number) { ... }
#=> [2,3].inject(6) { ... }
which may be a bit niftier, but the choice is yours.
The second way is to use inject
without an argument:
def subtract(*numbers)
numbers.inject { |current_result, number| current_result - number }
end
numbers = [6,2,3]
subtract(*numbers) #=> 1
You can see why this works by reviewing the doc for inject
.
Moreover, similar to :add
, you can write:
def subtract(*numbers)
numbers.inject :-
end
Lastly, subtract
requires numbers
to have at least one element, so we might write:
def subtract(*numbers)
raise ArgumentError, "argument cannot be an empty array" if numbers.empty?
numbers.inject :-
end
calculate(*arguments)
We see that calculate
expects to be invoked in one of the following ways:
calculate(6,2,3,{ :add=>true }) #=> 11
calculate(6,2,3,{ :add=>7 }) #=> 11
calculate(6,2,3,{ :subtract=>true }) #=> 1
calculate(6,2,3,{ :subtract=>7 }) #=> 1
calculate(6,2,3) #=> 11
If the hash has a key :add
with a "truthy" value (anything other than false
or nil
, we are to add; if the hash has a key :subtract
with a "truthy" value (anything other than false
or nil
, we are to subtract. If the last element is not a hash (calculate(6,2,3)
), add
is assumed.
Note:
calculate(6,2,3,{ :add=>false }) #=> nil
calculate(6,2,3,{ :subtract=>nil }) #=> nil
Let's write the method like this:
def calculate(*arguments)
options =
if arguments.last.is_a?(Hash) # or if arguments.last.class==Hash
arguments.pop
else
{}
end
if (options.empty? || options[:add])
add *arguments
elsif options[:subtract]
subtract *arguments
else
nil
end
end
calculate(6,2,3,{ :add=>true }) #=> 11
calculate(6,2,3,{ :add=>7 }) #=> 11
calculate(6,2,3,{ :subtract=>true }) #=> 1
calculate(6,2,3,{ :subtract=>7 }) #=> 1
calculate(6,2,3) #=> 11
calculate(6,2,3,{ :add=>false }) #=> nil
calculate(6,2,3,{ :subtract=>nil }) #=> nil
Note the return
keyword is not needed (nor is it needed in the original code). It seems very odd that a hash would be used to signify the type of operation to be performed. It would make more sense to invoke the method:
calculate(6,2,3,:add) #=> 11
calculate(6,2,3) #=> 11
calculate(6,2,3,:subtract) #=> 1
We can implement that as follows:
def calculate(*arguments)
operation =
case arguments.last
when :add
arguments.pop
:add
when :subtract
arguments.pop
:subtract
else
:add
end
case operation
when :add
add *arguments
else
subtract *arguments
end
end
Better:
def calculate(*arguments, op=:add)
case op
when :subtract
subtract *arguments
else
add *arguments
end
end
calculate(6,2,3,:add) #=> 11
calculate(6,2,3) #=> 11
calculate(6,2,3,:subtract) #=> 1
I am overwhelmed by your offer to be "endlessly and eternally thankful", but if you appreciate my efforts for a few minutes, that is sufficient.
Upvotes: 4
Reputation: 114178
current_result = numbers.shift
. Why is it there? I mean, I understand what it does, but what's its job in this particular piece of code?
The line removes the first element from the numbers
array and assigns it to current_result
. Afterwards, current_result - number
is calculated for each number
in the remaining numbers
array.
Example with logging:
numbers = [20, 3, 2]
current_result = numbers.shift
current_result #=> 20
numbers #=> [3, 2]
numbers.inject(current_result) do |current_result, number|
(current_result - number).tap do |result|
puts "#{current_result} - #{number} = #{result}"
end
end
Output:
20 - 3 = 17
17 - 2 = 15
15
would be the method's return value.
However, removing the first element is not necessary; inject
does this by default:
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.
Thus, the subtract
method can be simplified:
def subtract(*numbers)
numbers.inject { |current_result, number| current_result - number }
end
subtract(20, 3, 2) #=> 15
You can also provide a method symbol:
def subtract(*numbers)
numbers.inject(:-)
end
Same works for add
:
def add(*numbers)
numbers.inject(:+)
end
Upvotes: 3