Search code examples
rubyenumerable

Why would #each_with_object and #inject switch the order of block parameters?


#each_with_object and #inject can both be used to build a hash.

For example:

matrix = [['foo', 'bar'], ['cat', 'dog']]

some_hash = matrix.inject({}) do |memo, arr|
  memo[arr[0]] = arr
  memo # no implicit conversion of String into Integer (TypeError) if commented out
end
p some_hash # {"foo"=>["foo", "bar"], "cat"=>["cat", "dog"]}

another_hash = matrix.each_with_object({}) do |arr, memo|
  memo[arr[0]] = arr
end
p another_hash # {"foo"=>["foo", "bar"], "cat"=>["cat", "dog"]}

One of the key differences between the two is #each_with_object keeps track of memo through the entire iteration while #inject sets memo equal to the value returned by the block on each iteration.

Another difference is the order or the block parameters.

Is there some intention being communicated here? It doesn't make sense to reverse the block parameters of two similar methods.


Solution

  • They have a different ancestry.

    • each_with_object has been added to Ruby 1.9 in 2007
    • inject goes back to Smalltalk in 1980

    I guess if the language were designed with both methods from the begin they would likely expect arguments in the same order. But this is not how it happened. inject has been around since the begin of Ruby whereas each_with_object has been added 10 years later only.

    inject expects arguments in the same order as Smalltalk's inject:into:

    collection inject: 0 into: [ :memo :each | memo + each ]
    

    which does a left fold. You can think of the collection as a long strip of paper that is folded up from the left and the sliding window of the fold function is always the part that has already been folded plus the next element of the remaining paper strip

    # (memo = 0 and 1), 2, 3, 4  
    # (memo = 1 and 2), 3, 4                 
    # (memo = 3 and 3), 4                    
    # (memo = 6 and 4)                      
    

    Following the Smalltalk convention made sense back then since all the initial methods in Enumerable are taken from Smalltalk and Matz did not want to confuse people who are familiar with Smalltalk.

    Nor could anyone have the foresight to know that would happen in 2007 when each_with_object was introduced to Ruby 1.9 and the order of argument reflects the lexical order of the method name, which is each ... object.

    And hence these two methods expect arguments in different orders for historical reasons.