reductions 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|