reductions 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +70 -0
- data/benchmarks/run_benchmarks.rb +39 -0
- data/lib/reductions.rb +162 -0
- data/tests/unit_tests.rb +72 -0
- metadata +53 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c4dcd97fa68d0d05cac8b5620fd5d534b0467708
|
4
|
+
data.tar.gz: 2b05ad9ba6f3068e4d1523ae908f1c76d0ff6670
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 92a36ebb66b117705f7b5059398b5b20d0cc38dcc377805a197fe6328805c63780d122860dac17c01bceece4c4317ae8beac8ed91ba31bfa0a277618704dd8f7
|
7
|
+
data.tar.gz: af67b82c462547faf25e71feacb48b229909677717fb72c8f2c3cd271f1d9b620ab04140259a59d3e8fd89d07d045d46aee300a93ec16ef9e29832904d623088
|
data/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
Reductions
|
2
|
+
==========
|
3
|
+
|
4
|
+
Reductions is an addition to Ruby's Enumerable module that returns
|
5
|
+
an array containing all of the intermediate values that would be
|
6
|
+
generated in a call to Enumerable#reduce.
|
7
|
+
|
8
|
+
Installation
|
9
|
+
============
|
10
|
+
|
11
|
+
`gem install reductions`
|
12
|
+
|
13
|
+
Usage
|
14
|
+
=====
|
15
|
+
|
16
|
+
require 'reductions'
|
17
|
+
|
18
|
+
(5..10).reductions(:+) # => [5, 11, 18, 26, 35, 45]
|
19
|
+
(5..10).reductions{|a,b| a + b} # => [5, 11, 18, 26, 35, 45]
|
20
|
+
|
21
|
+
# (5..10).reduce(:+) would result in 45
|
22
|
+
|
23
|
+
**If you have monkey-patched Enumerable#reduce** and need to
|
24
|
+
ensure #reductions works according to your patched #reduce, pass
|
25
|
+
`use_reduce: true` to reductions:
|
26
|
+
|
27
|
+
(5..10).reductions(:+,use_reduce: true)
|
28
|
+
(5..10).reductions(use_reduce: true){|a,b| a + b}
|
29
|
+
|
30
|
+
Using this flag increases the required execution time. See Benchmarking
|
31
|
+
section for performance difference.
|
32
|
+
|
33
|
+
|
34
|
+
Why a gem for this?
|
35
|
+
-------------------
|
36
|
+
|
37
|
+
You _could_ just provide a block to Enumerable#reduce that does the same
|
38
|
+
thing:
|
39
|
+
|
40
|
+
(5..10).reduce([]){|acc,n| acc += acc.last ? [acc.last + n] : [n] }
|
41
|
+
|
42
|
+
However, a gem is preferable for two key reasons:
|
43
|
+
|
44
|
+
* It allows for more compact, more understandable code. Compare the above with
|
45
|
+
`(5..10).reductions(:+)`.
|
46
|
+
* There are significant performance problems with the "provide a block to reduce"
|
47
|
+
approach once you get into reductions with tens of thousands of elements. See
|
48
|
+
"naïve reductions" in the Benchmarking section.
|
49
|
+
|
50
|
+
|
51
|
+
Benchmarking
|
52
|
+
------------
|
53
|
+
The case being benchmarked here is `(1..100000).reductions{|a,b| a + b}`
|
54
|
+
|
55
|
+
user system total real
|
56
|
+
reduce 0.010000 0.000000 0.010000 ( 0.017606)
|
57
|
+
reductions 0.040000 0.000000 0.040000 ( 0.036335)
|
58
|
+
reductions use_reduce 0.050000 0.000000 0.050000 ( 0.051783)
|
59
|
+
naïve reductions 15.190000 0.530000 15.720000 ( 15.699648)
|
60
|
+
|
61
|
+
* `reduce` is a plain #reduce, included here to give a reference point for performance.
|
62
|
+
* `reductions` is the reduction using the exact case shown above.
|
63
|
+
Notice the full reduction --which returns an array
|
64
|
+
with 100,000 entries-- took only **~2** times as long as a plain reduce!
|
65
|
+
* `reductions use_reduce` is the reduction with the `use_reduce` flag set to
|
66
|
+
true. This reduction uses #reduce in its implementation.
|
67
|
+
* `naïve reductions` is the reduction using reduce as shown in the "Why a gem for this?"
|
68
|
+
section. The large number of incremental Array allocations makes
|
69
|
+
it _really_ slow, in this case taking more than 400 times as long
|
70
|
+
as the `reductions` version.
|
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
require 'reductions'
|
5
|
+
|
6
|
+
# For comparison purposes, monkey-patch in a naive implementation. Ignore all
|
7
|
+
# the setup and just look at the two return statements to understand what this
|
8
|
+
# is doing.
|
9
|
+
module Enumerable
|
10
|
+
def naive_reductions(init=nil,sym=nil,&block)
|
11
|
+
if sym == nil && block == nil && (init.kind_of?(Symbol) || init.kind_of?(String))
|
12
|
+
sym = init
|
13
|
+
init = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
raise LocalJumpError.new("no block given") if !sym && !block
|
17
|
+
|
18
|
+
iv = init ? [init] : []
|
19
|
+
if block
|
20
|
+
return self.reduce(iv){|acc,n| acc += acc.last ? [yield(acc.last,n)] : [n]}
|
21
|
+
else
|
22
|
+
return self.reduce(iv){|acc,n| acc += acc.last ? [acc.last.method(sym).call(n)] : [n]}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# And now the benchmarking:
|
29
|
+
|
30
|
+
br = (1..100000)
|
31
|
+
ba = br.to_a
|
32
|
+
|
33
|
+
Benchmark.bmbm(29) do |b|
|
34
|
+
b.report('100K reduce') {ba.reduce{|a,n| a + n}}
|
35
|
+
b.report('100K reductions direct access') {ba.reductions{|a,n| a + n}}
|
36
|
+
b.report('100K reductions each-style') {br.reductions{|a,n| a + n}}
|
37
|
+
b.report('100K reductions use_reduce') {br.reductions(use_reduce: true){|a,n| a + n}}
|
38
|
+
b.report('100K naive reductions') {br.naive_reductions{|a,n| a + n}}
|
39
|
+
end
|
data/lib/reductions.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
|
2
|
+
module Enumerable
|
3
|
+
|
4
|
+
# Returns an Array containing all the intermediate values collected when
|
5
|
+
# performing +Enumerable#reduce+. It accepts the same parameters and block
|
6
|
+
# as +Enumerable#reduce+, with the addition of the named parameter +use_reduce+
|
7
|
+
#
|
8
|
+
# *WARNING:* If you have monkey-patched +Enumerable#reduce+, you should pass
|
9
|
+
# +use\_reduce: true+ in the parameter list.
|
10
|
+
#
|
11
|
+
# @param [Object] init an initial value for the reduction. Just like the one for reduce....
|
12
|
+
#
|
13
|
+
# @param [Symbol] sym symbol representing a binary operation. Just like the one for reduce....
|
14
|
+
#
|
15
|
+
# @param [Bool] use_reduce: (named parameter) uses Enumerable#reduce in the
|
16
|
+
# implementation if true. Defaults to false for faster execution.
|
17
|
+
#
|
18
|
+
# @param [Block] block a block to use instead of a symbol. Just like the one for reduce....
|
19
|
+
#
|
20
|
+
# @return [Array] the array of intermediate values
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# # These attempt to follow the examples used for Enumerable#reduce
|
24
|
+
#
|
25
|
+
# # Sum some numbers
|
26
|
+
# (5..10).reductions(:+) # => [5, 11, 18, 26, 35, 45]
|
27
|
+
# # Multiply some numbers
|
28
|
+
# (5..10).reductions(1, :*) # => [1, 5, 30, 210, 1680, 15120, 151200]
|
29
|
+
# # Same using a block
|
30
|
+
# (5..10).reductions(1) { |product, n| product * n } # => [1, 5, 30, 210, 1680, 15120, 151200]
|
31
|
+
# # Force the use of Enumerable#reduce
|
32
|
+
# (5..10).reductions(1, :*, use_reduce: true) # => [1, 5, 30, 210, 1680, 15120, 151200]
|
33
|
+
#
|
34
|
+
def reductions(init=nil,sym=nil,use_reduce: false,&block)
|
35
|
+
|
36
|
+
if sym == nil && block == nil && (init.kind_of?(Symbol) || init.kind_of?(String))
|
37
|
+
sym = init
|
38
|
+
init = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
raise LocalJumpError.new("no block given") if !sym && !block
|
42
|
+
|
43
|
+
sz = get_reductions_prealloc_size
|
44
|
+
|
45
|
+
acc = init ? Array.new(sz + 1) : Array.new(sz)
|
46
|
+
|
47
|
+
# Use the slower version if requested
|
48
|
+
return reductions2(init: init,sym: sym,acc: acc,&block) if use_reduce
|
49
|
+
|
50
|
+
# Direct access reductions with no initial value are faster than
|
51
|
+
# each style reductions with no initial value because the latter
|
52
|
+
# are forced to perform a boolean test at each iteration
|
53
|
+
direct = self.respond_to?(:[])
|
54
|
+
|
55
|
+
# presence or absence of init changes how we treat index
|
56
|
+
# We use +yield+ rather than +block.call+ wherever possible since yield
|
57
|
+
# executes with less overhead.
|
58
|
+
if block && !init
|
59
|
+
if direct
|
60
|
+
acc[0] = self[0]
|
61
|
+
sz -= 1
|
62
|
+
sz.times do |j|
|
63
|
+
acc[j+1] = yield(acc[j],self[j+1])
|
64
|
+
end
|
65
|
+
else
|
66
|
+
self.each_with_index do |k,j|
|
67
|
+
if j == 0
|
68
|
+
acc[0] = k
|
69
|
+
else
|
70
|
+
acc[j] = yield(acc[j-1],k)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
elsif block && init
|
76
|
+
acc[0] = init
|
77
|
+
self.each_with_index do |k,j|
|
78
|
+
acc[j+1] = yield(acc[j],k)
|
79
|
+
end
|
80
|
+
|
81
|
+
elsif !block && !init
|
82
|
+
if direct
|
83
|
+
acc[0] = self[0]
|
84
|
+
sz -= 1
|
85
|
+
sz.times do |j|
|
86
|
+
acc[j+1] = acc[j].method(sym).call(self[j+1])
|
87
|
+
end
|
88
|
+
else
|
89
|
+
self.each_with_index do |k,j|
|
90
|
+
if j == 0
|
91
|
+
acc[0] = k
|
92
|
+
else
|
93
|
+
acc[j] = acc[j-1].method(sym).call(k)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
elsif !block && init
|
99
|
+
acc[0] = init
|
100
|
+
self.each_with_index do |k,j|
|
101
|
+
acc[j+1] = acc[j].method(sym).call(k)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
return acc
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
|
110
|
+
private
|
111
|
+
# Returns the allocation size we need for the results.
|
112
|
+
def get_reductions_prealloc_size
|
113
|
+
if self.respond_to?(:size)
|
114
|
+
return self.size
|
115
|
+
else
|
116
|
+
# Works for any Enumerable, but generally slower than #size
|
117
|
+
return self.count
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
private
|
123
|
+
# Variant of reductions that uses Enumerable#reduce directly in its
|
124
|
+
# implementation, while still getting the advantages of result array
|
125
|
+
# pre-allocation. Parameter checking and array allocation occurs in
|
126
|
+
# #reductions, which then optionally calls this function.
|
127
|
+
def reductions2(init: nil,sym: nil, acc: [],&block)
|
128
|
+
|
129
|
+
acc[0] = init if init
|
130
|
+
idx = init ? 1 : 0
|
131
|
+
iv = [idx,acc]
|
132
|
+
|
133
|
+
if block
|
134
|
+
return self.reduce(iv) do |acc,n|
|
135
|
+
idx = acc[0]
|
136
|
+
v = acc[1]
|
137
|
+
if idx == 0
|
138
|
+
v[0] = n
|
139
|
+
else
|
140
|
+
v[idx] = yield(v[idx - 1],n)
|
141
|
+
end
|
142
|
+
acc[0] = idx + 1
|
143
|
+
acc
|
144
|
+
end.last
|
145
|
+
else
|
146
|
+
return self.reduce(iv) do |acc,n|
|
147
|
+
idx = acc[0]
|
148
|
+
v = acc[1]
|
149
|
+
if idx == 0
|
150
|
+
v[0] = n
|
151
|
+
else
|
152
|
+
v[idx] = v[idx - 1].method(sym).call(n)
|
153
|
+
end
|
154
|
+
acc[0] = idx + 1
|
155
|
+
acc
|
156
|
+
end.last
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
(1..5).reductions(:+,use_reduce: true)
|
data/tests/unit_tests.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'reductions'
|
5
|
+
|
6
|
+
class TestReductions < Minitest::Test
|
7
|
+
|
8
|
+
def test_simple_block_reduction_esr
|
9
|
+
assert_equal [1,3,6,10,15], (1..5).reductions{|a,b| a + b}
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_simple_block_reduction_dar
|
13
|
+
assert_equal [1,3,6,10,15], (1..5).to_a.reductions{|a,b| a + b}
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_simple_block_reduction2
|
17
|
+
assert_equal [1,3,6,10,15], (1..5).reductions(use_reduce: true){|a,b| a + b}
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_simple_sym_reduction_esr
|
21
|
+
assert_equal [1,3,6,10,15], (1..5).reductions(:+)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_simple_sym_reduction_dar
|
25
|
+
assert_equal [1,3,6,10,15], (1..5).to_a.reductions(:+)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_simple_sym_reduction2
|
29
|
+
assert_equal [1,3,6,10,15], (1..5).reductions(:+,use_reduce: true)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_simple_string_reduction_esr
|
33
|
+
assert_equal [1,3,6,10,15], (1..5).reductions('+')
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_simple_string_reduction_dar
|
37
|
+
assert_equal [1,3,6,10,15], (1..5).to_a.reductions('+')
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_simple_string_reduction2
|
41
|
+
assert_equal [1,3,6,10,15], (1..5).reductions('+',use_reduce: true)
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_init_block_reduction_esr
|
45
|
+
assert_equal [2,3,5,8,12,17], (1..5).reductions(2){|a,b| a + b}
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_init_block_reduction_dar
|
49
|
+
assert_equal [2,3,5,8,12,17], (1..5).to_a.reductions(2){|a,b| a + b}
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_init_block_reduction2
|
53
|
+
assert_equal [2,3,5,8,12,17], (1..5).reductions(2,use_reduce: true){|a,b| a + b}
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_init_sym_reduction_esr
|
57
|
+
assert_equal [2,3,5,8,12,17], (1..5).reductions(2,:+)
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_init_sym_reduction_dar
|
61
|
+
assert_equal [2,3,5,8,12,17], (1..5).to_a.reductions(2,:+)
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_init_sym_reduction2
|
65
|
+
assert_equal [2,3,5,8,12,17], (1..5).reductions(2,:+,use_reduce: true)
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_raises_no_block
|
69
|
+
assert_raises(LocalJumpError){ (1..5).reductions }
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
metadata
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: reductions
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Penn Taylor
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-03-13 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |2
|
14
|
+
Reductions is an addition to Enumerable that returns
|
15
|
+
an array containing all of the intermediate values that would
|
16
|
+
be generated in a call to Enumerable#reduce.
|
17
|
+
|
18
|
+
(5..10).reductions(:+) # => [5, 11, 18, 26, 35, 45]
|
19
|
+
email: rpenn3@gmail.com
|
20
|
+
executables: []
|
21
|
+
extensions: []
|
22
|
+
extra_rdoc_files: []
|
23
|
+
files:
|
24
|
+
- README.md
|
25
|
+
- benchmarks/run_benchmarks.rb
|
26
|
+
- lib/reductions.rb
|
27
|
+
- tests/unit_tests.rb
|
28
|
+
homepage: https://github.com/penntaylor/reductions
|
29
|
+
licenses:
|
30
|
+
- MIT
|
31
|
+
metadata: {}
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
require_paths:
|
35
|
+
- lib
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
requirements: []
|
47
|
+
rubyforge_project:
|
48
|
+
rubygems_version: 2.4.6
|
49
|
+
signing_key:
|
50
|
+
specification_version: 4
|
51
|
+
summary: Adds reductions capability to Enumerable
|
52
|
+
test_files: []
|
53
|
+
has_rdoc:
|