Allow additional parameters to Enumerable#inject


#1

Hi,

I wanted to count consecutive elements in an array:

[1,1,1,2,2,2,2,3,4,4,4,4].count_streams #=> [3,4,1,4]

module Enumerable
def count_streams
last_seen = nil
self.inject([]) { |a, elem|
if last_seen == elem then a[-1] += 1 else a << 1 end
last_seen = elem
a
}
end
end

However, the assignment to “last” outside of the block seems ugly, so
I changed inject to take additional parameters:

module Enumerable
# works just like regular inject, but passes the additional
parameters
# to the block
def inject_with_state(memo, *other)
self.each { |obj|
memo, *other = yield(memo, obj, *other)
}
memo
end

def count_streams
  self.inject_with_state([], nil) { |a, elem, last_seen|
    if last_seen == elem then a[-1] += 1 else a << 1 end
    [a, elem]
  }
end

end

Is this a good solution to the problem?

I think that inject_with_state could be made fully backwards
compatible to inject, it would be nice if inject could be changed to
support this. What do you think?

Viele Grü�e,
Levin


#2

An array can be used as a workaround:

[1,2,3].inject([[],arg]) { |(a,b), elem|

[a,arg]
}

lopex


#3

Levin A. wrote:

    if last_seen == elem then a[-1] += 1 else a << 1 end
# works just like regular inject, but passes the additional
    if last_seen == elem then a[-1] += 1 else a << 1 end
    [a, elem]
  }
end

end

Is this a good solution to the problem?

I think that inject_with_state could be made fully backwards
compatible to inject, it would be nice if inject could be changed to
support this. What do you think?

You don’t need to redefine Enumerable#inject for this.

def count_streams(enum)
enum.inject([[],nil]) {|(a, last),e| e == last ? a[-1]+=1 : a<<1; [a,
e]}[0]

end
=> nil

count_streams [1,1,1,2,2,2,2,3,4,4,4,4]
=> [3, 4, 1, 4]

You can as well do somehting like this:

def count_streams_2(enum)
enum.inject([]) do |a, e|
?> a.empty? || e != a[-1][0] ? a << [e,1] : a[-1][1]+=1

a

end
end
=> nil

count_streams_2 [1,1,1,2,2,2,2,3,4,4,4,4]
=> [[1, 3], [2, 4], [3, 1], [4, 4]]

count_streams_2([1,1,1,2,2,2,2,3,4,4,4,4]).map {|a| a[1]}
=> [3, 4, 1, 4]

Also, I’m not sure it’s a good idea to put this method in Enumerable.
Is
it really general enough?

Kind regards

robert

#4

On 2/3/06, Robert K. removed_email_address@domain.invalid wrote:

I think that inject_with_state could be made fully backwards
compatible to inject, it would be nice if inject could be changed to
support this. What do you think?

You don’t need to redefine Enumerable#inject for this.

def count_streams(enum)
enum.inject([[],nil]) {|(a, last),e|

Thanks, I was not aware that parameters can be grouped like that.

But I think that an Enumerable#inject with additional parameters would
also be useful for many other things, like removing duplicates:

def remove_duplicates1(enum)
enum.inject([[], nil]) {|(arr, last),elem| arr << elem unless elem
== last; [arr, elem] }[0]
end
def remove_duplicates2(enum)
enum.inject([], nil) {|(arr, elem, last| arr << elem unless elem
== last; [arr, elem] }
end

The second one seems looks a lot cleaner.

Also, I’m not sure it’s a good idea to put this method in Enumerable. Is
it really general enough?

No, it probably is not. This was just a for small experiment (*), i
would not put this into a library.
(However, it seems to be preferred to put methods into the class
“where they belong” in Ruby instead of using helper classes)

Thank you,
Levin

(*) http://www.rexswain.com/benford.html
“…It showed, he said, that the overwhelming odds are that at some
point in a series of 200 tosses, either heads or tails will come up
six or more times in a row.”

(0…10_000).select {
(0…200).map { rand 2 }.inject([[],nil]) {|(a,last),e|
if e==last then a[-1]+=1 else a << 1 end
[a,e]
}[0].partition { |e| e >= 6 }.first.empty?
}.length / 10_000.0
#=> 0.0349

“overwhelming odds” --> 97%


#5

Not really about inject specifically… just attempting another way

a = [1,1,1,2,2,2,2,3,4,4,4,4]

a.zip([0] + a).map { |i, j| i - j }.inject([]) do |s, k|
k.zero? ? s[0…-1] + [s[-1] + 1] : s << k
end