2

I'm a confused with what .each_with_object does to an extent.

For example:

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

Also:

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

(since integers are immutable).


After reading the example in the Ruby documentation, I'm confused as to what the parameter inside object() actually does.

Regarding the flattify code below: I was confused with the usage of *; and why is the else statement just element? What is element intended to do?

def flattify(array)
  array.each_with_object([]) do |element, flattened|
    flattened.push *(element.is_a?(Array) ? flattify(element) : element)
  end
end
Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
Muntasir Alam
  • 1,777
  • 2
  • 17
  • 26

4 Answers4

1

Here element will take value of each array element consequently.
All this piece of code does is just recursively flat all nested arrays inside initial array.
But Ruby has built in flatten method which does the same thing.
For example

ar = [1, 2, [3, 4, [5, 6]]]
ar.flatten
#=> [1, 2, 3, 4, 5, 6]

Just to compare with your flattify

flattify ar
#=> [1, 2, 3, 4, 5, 6]
Aleksey
  • 2,289
  • 17
  • 27
  • Doesent the line there mean IF element is an array = > flattify our element in the array. Else = > element? How can we just have else element. Also what does the * do.? – Muntasir Alam Jun 27 '16 at 18:37
  • Yes, it is called _ternary operator_. As other answers mentioned `*` is a splat operator. It converts array to sequence of separate values. This way `push` can receive multiple elements to be pushed. – Aleksey Jun 27 '16 at 18:39
  • I know that. But how can we just else element. We are just saying else, variable? Dont we usually do something like else {do some function like puts 5 etc etc} – Muntasir Alam Jun 27 '16 at 18:40
  • It will be like: push recursive `flattify(element)` or push simple value, i.e. `element`. – Aleksey Jun 27 '16 at 18:42
  • I dont get that line.. Sorry is there any othe way to explain? – Muntasir Alam Jun 27 '16 at 18:50
  • I recommend you just use `flatten`. It is not the best practice to write strange custom things)) – Aleksey Jun 27 '16 at 18:56
1

The parameter you pass to object() acts as accumulator for intermediate values between iterations. On entry to each iteration it is passed as flattened argument.

* is a splat operator. It converts an array to a list of arguments being passed to the push method.

Nic Nilov
  • 5,056
  • 2
  • 22
  • 37
  • Do you mean to say that every time the loop runs the parameter inside object is added. I.E we add [] into flattened every iteration? – Muntasir Alam Jun 27 '16 at 18:46
  • Yes, the same object which was initially passed as the argument to `each_with_object()` is becoming available by the name `flattened` within each iteration step. Anything you do with it will be kept for the next iterations and the object will eventually be returned from `each_with_object` call. – Nic Nilov Jun 27 '16 at 18:49
  • I'm a bit confused. Here we use flattened as an array. But how does our code know flattened is an array if we didnt set it to one? My thought was that object([]) initializes the second parameter to an array – Muntasir Alam Jun 27 '16 at 18:52
  • Exactly. You pass an empty array to `each_with_object` and it becomes the initial state of the accumulator. What happens behind the scenes is ruby takes this empty array and gives it back to you in form of the `flattened` parameter. You can pass any object to `each_with_object`, not necessarily an array. It is up to your iteration code to use it how you see fit. – Nic Nilov Jun 27 '16 at 18:55
  • And our code sets element to an integer by default? It starts at 1 or 0? – Muntasir Alam Jun 27 '16 at 18:58
  • The `element` will contain each subsequent element of the array you call `each_with_object` on, starting from index `0`. – Nic Nilov Jun 27 '16 at 19:01
  • if we put a third parameter in the loop would that be an integer as well? or can we not do that? – Muntasir Alam Jun 27 '16 at 19:04
  • Depending on the nature of your `Enumerable` object you can pass more arguments to the loop but the last argument will still be the accumulator. See [Enumerable docs](http://ruby-doc.org/core-2.3.1/Enumerable.html#method-i-each_with_object) on that. – Nic Nilov Jun 27 '16 at 19:08
  • Great. Now i'm just confused about : element, why does our else statement just have element. We have if our element is an array flatten it, otherwise element. I don't know what that last part is supposed to imply – Muntasir Alam Jun 27 '16 at 19:19
  • By the way what exactly is taking out the [] brackets? I don't see anything like that in the code – Muntasir Alam Jun 27 '16 at 19:21
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/115760/discussion-between-nic-nilov-and-cresjoy). – Nic Nilov Jun 27 '16 at 19:29
  • Do you have any other simple example of using the splat operator? I can't seem to find any super basic ones – Muntasir Alam Jun 28 '16 at 17:43
  • You should ask a separate question on that if you can't find one answered. – Nic Nilov Jun 28 '16 at 17:46
  • You said splat converts an array to a list of arguments. Why do we need it here? Aren't we looping and each time pushing it inside the array. It doesent seem necessary – Muntasir Alam Jun 28 '16 at 17:49
1

confused with what #each_with_object does

You may have a better time understanding #each_with_object if you look at #inject first. #each_with_object is similar to #inject. Examples from http://blog.krishnaswamy.in/blog/2012/02/04/ruby-inject-vs-each-with-object/, included below:

#using inject
[[:tom,25],[:jerry,15]].inject({}) do |result, name_and_age|
  name, age = name_and_age
  result[name] = age
  result
end

=> {:tom=>25, :jerry=>15}


#using each_with_object
[[:tom,25],[:jerry,15]].each_with_object({}) do |name_and_age, result|
  name, age = name_and_age
  result[name] = age
end

=> {:tom=>25, :jerry=>15}

See this Gist for example tests: https://gist.github.com/cupakromer/3371003

In depth article: http://engineering-blog.alphasights.com/tap-inject-and-each_with_object/


UPDATE

would #inject as opposed to #each_with_object work in this flattening code?

Yes, see below. I've illustratively refactored your flattening code to use #inject. Additionally, I removed the dependency on the "splat" operator (http://ruby-doc.org/core-2.3.1/doc/syntax/calling_methods_rdoc.html#label-Array+to+Arguments+Conversion)

# Flattens nested array; uses `Enumerable#inject`
# @see http://ruby-doc.org/core-2.3.1/Enumerable.html#method-i-inject
# @param arg [Array] contains objects of any type including any amount of nested arrays.
# @raise [StandardError] if arg is not Array class
# @return [Array] flat array comprised of elements from arg.
# @example
#   flattify([nil, [1, [:two, [3.0], {4=>5}], "6"]]) #=> [nil, 1, :two, 3.0, {4=>5}, "6"]
def flattify(arg)
  raise "arg is not Array" unless arg.is_a?(Array)

  # variable ret_var used here to illustrate method's return in verbose fasion
  # supplied [] used as initial value for flattened_array
  ret_var = arg.inject([]) do |flattened_array, element|
    # check if element class is Array
    if element.is_a?(Array)
      # Array#concat because flattify returns Array
      # same as: a = a + b
      # same as: a += b
      flattened_array.concat(
        # recursively call flattify with element as arg
        # element is an Array
        flattify(element)
      )
    else
      # Array#push because element is not an Array
      # same as: a << b
      flattened_array.push(element)
    end

    # used in next iteration as value for first arg above in: "|flattened_array, element|"
    # OR returned on last iteration, becoming value of ret_var above
    flattened_array
  end

  # explicit return for illustrative purposes
  return ret_var
end

UPDATE 2

may [I] ask why the splat operator is used here? I am still a bit confused on that. It seems the code is [looping] each time and pushing it in the flattened array, whats the point of the *?

flattened.push *(element.is_a?(Array) ? flattify(element) : element)

The above block is a "ternary operation" (see: https://en.wikipedia.org/wiki/Ternary_operation), which is explained here: https://stackoverflow.com/a/4252945/1076207 like so:

if_this_is_a_true_value ? then_the_result_is_this : else_it_is_this

Compare the flattify examples with each other:

# each_with_object
      flattened.push *(flattify(element))
# inject
flattened_array.concat(flattify(element))

Here the * splat operator (see: https://stackoverflow.com/search?q=%5Bruby%5D+splat) is doing the same thing as Array#concat. However, the splat allows flattened.push to accept either of the two possible types the ternary operation returns: 1) an Array; or 2) whatever element is. For illustration, notice how the splat operator prevents nesting:

# each_with_object with splat
      flattened = [1,2,3]
      flattened.push *([4,5,6])   # => [1, 2, 3, 4, 5, 6]
      flattened.push *(7)         # => [1, 2, 3, 4, 5, 6, 7]

# each_with_object without splat
      flattened = [1,2,3]
      flattened.push  ([4,5,6])   # => [1, 2, 3, [4, 5, 6]]
      flattened.push  (7)         # => [1, 2, 3, [4, 5, 6], 7]

Conversely, Array#concat will only accept an array. If the same ternary operation was used and returned an element, it would cause an error:

# inject
flattened_array = [1,2,3]
flattened_array.concat([4,5,6])   # => [1, 2, 3, 4, 5, 6]
flattened_array.concat(7)         # => TypeError: no implicit conversion of Fixnum into Array

In summary, both versions of flattify achieve the same result. However, #each_with_object uses #push, a ternary operation and a splat operator; while #inject uses an if/else statement, #concat and #push.


UPDATE 3

When we did each with object([]), the last parameter became an array.

Yes. It becomes an array and continues to be that same array throughout the iterations until it's passed back.

So with inject the first one becomes an array?

Yes. The first one becomes the passed in array, but only for the first iteration, then it's replaced by the result of the code block for each subsequent iteration.

enter image description here

how does our code know if element is defined as an int and flattened_Array is an array?

element.is_a?(Array) # => true or false

When element is Array class this method returns true, and if not returns false. false means that it's anything but an array including int.

For more info, see: http://ruby-doc.org/core-2.3.1/Object.html#method-i-is_a-3F

Community
  • 1
  • 1
SoAwesomeMan
  • 3,226
  • 1
  • 22
  • 25
  • 1
    The main difference between `inject` and `each_with_object` is that the latter **mutates** the accumulator, while `inject` creates the new object on each iteration, not “the memo passed to the next iteration is done automatically.” – Aleksei Matiushkin Jun 27 '16 at 20:44
  • @mudasobwa Agreed. I updated my answer to remove my hasty explanation of how `#inject` sets the memo with the block's return value – SoAwesomeMan Jun 27 '16 at 22:01
  • would inject as opposed to each with object work in this flattening code? – Muntasir Alam Jun 27 '16 at 22:26
  • @cresjoy Good question. I've updated my answer with an example of using `inject`. – SoAwesomeMan Jun 28 '16 at 03:11
  • It seems injection is much less efficient as you have to create a new object every time – Muntasir Alam Jun 28 '16 at 15:35
  • @cresjoy Both seem equally efficient, with `#inject` ahead by a little. Have a look at some benchmarks: https://gist.github.com/mjgiarlo/7680957 and http://phrogz.net/tap-vs-each_with_object – SoAwesomeMan Jun 28 '16 at 16:02
  • I see. Thanks, may i ask why the splat operator is used here? I am still a bit confused on that. It seems the code is loopign each time and pushing it in the flattened array, whats the point of the *? – Muntasir Alam Jun 29 '16 at 00:14
  • @cresjoy I've updated my answer with info about the splat operator in context of `flattify`. – SoAwesomeMan Jun 29 '16 at 02:00
  • Simply amazing. As a beginner to ruby this has helped immensely. Do you know of a book where I can read and actually solve problems? I've found some books , but I actually need exercises to practise. – Muntasir Alam Jun 29 '16 at 10:44
  • Since we did inject([]), is that how the compiler knows flattened_Array is an actual array? As you know when we did each with object ([]), the last parameter became an array. So with inject the first one becomes an array? I,e how does our code know if element is defined as an int and flattened_Array isan array – Muntasir Alam Jun 29 '16 at 12:25
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/115971/discussion-between-soawesomeman-and-cresjoy). – SoAwesomeMan Jun 29 '16 at 13:24
  • @cresjoy I've updated my answer with further explanation of the subject matter. That's as far as I can take you in this answer. You're on the right track! – SoAwesomeMan Jun 29 '16 at 16:58
  • 1
    Thanks, I get it now. I was watching a tutorial online on inject. Totally makes sense. Things are really hard to understand when explained over words, but for some reason a video makes it so easy :P – Muntasir Alam Jul 01 '16 at 16:00
1
# flattened.push *(element.is_a?(Array) ? flattify(element) : element)
# flattened is the array ...object([])
# element.is_a?(Array) ...is the element in this iteration an array?
# if true flattify(element) again... meaning recursively apply method again
# if false push element onto the object([]) aka flattened
# the () around the ternary allow the recursion to complete
# the * operator can then pass the elements "passing the array condition"
# cont'd... onto flattened.push(4, 5, 6) as list of args instead of an array


# array object with range of string elements
("a".."c").each_with_object([]) do |element, the_object|
  p the_object.class # returns Array 
  p element.class # returns String
end

# hash object with range of fixnum elements
(1..3).each_with_object({}) do |element, the_object|
  p the_object.class # returns Hash
  p element.class # returns Fixnum
end
ventured1
  • 31
  • 4