40

I'm trying to figure out how each_with_object is supposed to be used.

I have a sum example that doesn't work:

> (1..3).each_with_object(0) {|i,sum| sum+=i}
=> 0

I would assume that the result would be 6 ! Where is my mistake ?

notapatch
  • 6,569
  • 6
  • 41
  • 45
Orabîg
  • 11,718
  • 6
  • 38
  • 58
  • 1
    By the way. Can someone tell me why Ruby is not throwing some kind of error when doing `sum+=i`, ie. trying to modify an immutable object ? The whole point here is that the poor user is left alone with no clue that his command is just silently failing... – Orabîg May 22 '16 at 06:53

3 Answers3

74

each_with_object does not work on immutable objects like integer.

(1..3).each_with_object(0) {|i,sum| sum += i} #=> 0

This is because each_with_object iterates over a collection, passing each element and the given object to the block. It does not update the value of object after each iteration and returns the original given object.

It would work with a hash since changing value of a hash key changes it for original object by itself.

(1..3).each_with_object({:sum => 0}) {|i,hsh| hsh[:sum] += i}
#=> {:sum => 6}

String objects are interesting case. They are mutable so you might expect the following to return "abc"

("a".."c").each_with_object("") {|i,str| str += i} # => ""

but it does not. This is because str += "a" returns a new object and the original object stays the same. However if we do

("a".."c").each_with_object("") {|i,str| str << i} # => "abc"

it works because str << "a" modifies the original object.

For more info see ruby docs for each_with_object

For your purpose, use inject

(1..3).inject(0) {|sum,i| sum += i} #=> 6
# or
(1..3).inject(:+) #=> 6
tihom
  • 7,923
  • 1
  • 25
  • 29
  • 1
    Well, thanks. Indeed, I was reading the doc here : http://ruby-doc.org/core-2.0.0/Enumerable.html#method-i-each_with_object and there is no mention of such detail. Thanks. – Orabîg Sep 28 '13 at 06:54
  • The problem is that `sum+=i` is `sum = sum + i` and `sum = ...` won't change the `sum` that was passed in, it will just assign a new value to a local variable. `inject` works because `inject` feeds the block's return value back into the next iteration, you'd probably be better off writing `(1..3).inject(0) {|sum,i| sum + i }` or even `(1..3).inject(:+)` in this case, `sum += i` inside the `inject` block is misleading and confusing. – mu is too short Sep 28 '13 at 07:13
  • I would prefer `sum+i` as it more clear, but `each_with_object` will not work with that. I wanted show that the code OP had works with `inject` but not with `each_with_object` – tihom Sep 28 '13 at 07:20
  • But the whole approach won't work with `each_with_object` since `Fixnum`s are, as you've noted, immutable. `each_with_object` is simply the wrong tool for this job. – mu is too short Sep 28 '13 at 07:58
  • 2
    This is a great solution and these examples should be added to ruby docs! – Muntasir Alam Aug 25 '16 at 14:58
15

A simple, but common example of using each_with_object is when you need to build a hash depending on elements in an array. Very often you see something like:

hash = {}
[1, 2, 3, 4].each { |number| hash[number] = number**2 }
hash

Using each_with_object avoids the explicit initialization and return of the hash variable.

[1,2,3,4].each_with_object({}) { |number, hash| hash[number] = number**2 }

I advise reading the docs for inject, tap, and each_with_index. These methods are helpful when you aim for short and readable code.

spickermann
  • 100,941
  • 9
  • 101
  • 131
10

The documentation of Enumerable#each_with_object is very clear :

Iterates the given block for each element with an arbitrary object given, and returns the initially given object.

In your case, (1..3).each_with_object(0) {|i,sum| sum+=i},you are passing 0,which is immutable object. Thus here the intial object to each_with_object method is 0,so method returns 0.It works at it is advertised. See below how each_with_object works ,

(1..3).each_with_object(0) do |e,mem|
    p "#{mem} and #{e} before change"
    mem = mem + e
    p mem
end

# >> "0 and 1 before change"
# >> 1
# >> "0 and 2 before change"
# >> 2
# >> "0 and 3 before change"
# >> 3

That means in every pass,mem is set to initial passed object. You might be thinking of the in first pass mem is 0,then in next pass mem is the result of mem+=e,i.e. mem will be 1.But NO,in every pass mem is your initial object,which is 0.

Arup Rakshit
  • 116,827
  • 30
  • 260
  • 317