transducers 0.4.94
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.yardopts +6 -0
- data/Gemfile +8 -0
- data/LICENSE +202 -0
- data/README.md +46 -0
- data/Rakefile +116 -0
- data/benchmarks/arguments_to_filter.rb +59 -0
- data/benchmarks/arguments_to_map.rb +60 -0
- data/benchmarks/transducers_v_ruby_iterators.rb +158 -0
- data/bin/rspec-across-supported-versions +12 -0
- data/build/revision +14 -0
- data/dev/irb_tools.rb +36 -0
- data/lib/transducers.rb +615 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/transducers/reducer_spec.rb +49 -0
- data/spec/transducers_spec.rb +286 -0
- data/transducers.gemspec +34 -0
- metadata +151 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
# Copyright 2014 Cognitect. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
16
|
+
require 'transducers'
|
17
|
+
require 'benchmark'
|
18
|
+
|
19
|
+
class Inc
|
20
|
+
def process(n) n + 1 end
|
21
|
+
end
|
22
|
+
|
23
|
+
times = 100
|
24
|
+
size = 1000
|
25
|
+
|
26
|
+
e = 1.upto(size)
|
27
|
+
a = e.to_a
|
28
|
+
|
29
|
+
T = Transducers
|
30
|
+
|
31
|
+
Benchmark.benchmark do |bm|
|
32
|
+
{"enum" => e, "array" => a}.each do |label, coll|
|
33
|
+
puts "map with object (#{label})"
|
34
|
+
3.times do
|
35
|
+
bm.report do
|
36
|
+
times.times do
|
37
|
+
T.transduce(T.map(Inc.new), :<<, [], coll)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
puts "map with Symbol(#{label})"
|
43
|
+
3.times do
|
44
|
+
bm.report do
|
45
|
+
times.times do
|
46
|
+
T.transduce(T.map(:succ), :<<, [], coll)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
puts "map with block (#{label})"
|
52
|
+
3.times do
|
53
|
+
bm.report do
|
54
|
+
times.times do
|
55
|
+
T.transduce(T.map {|n| n + 1}, :<<, [], coll)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# Copyright 2014 Cognitect. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
16
|
+
require 'transducers'
|
17
|
+
require 'benchmark'
|
18
|
+
|
19
|
+
class Inc
|
20
|
+
def call(n) n + 1 end
|
21
|
+
end
|
22
|
+
|
23
|
+
class Even
|
24
|
+
def call(n) n.even? end
|
25
|
+
end
|
26
|
+
|
27
|
+
map_inc = Transducers.map(Inc.new)
|
28
|
+
filter_even = Transducers.filter(Even.new)
|
29
|
+
map_inc_filter_even = Transducers.compose(map_inc, filter_even)
|
30
|
+
e = 1.upto(1000)
|
31
|
+
a = e.to_a
|
32
|
+
|
33
|
+
times = 100
|
34
|
+
|
35
|
+
T = Transducers
|
36
|
+
|
37
|
+
Benchmark.benchmark do |bm|
|
38
|
+
{"enum" => e, "array" => a}.each do |label, coll|
|
39
|
+
puts "select (#{label})"
|
40
|
+
|
41
|
+
3.times do
|
42
|
+
bm.report do
|
43
|
+
times.times do
|
44
|
+
coll.select {|n| n.even?}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
puts "filter (#{label})"
|
50
|
+
3.times do
|
51
|
+
bm.report do
|
52
|
+
times.times do
|
53
|
+
T.transduce(filter_even, :<<, [], coll)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
puts
|
59
|
+
|
60
|
+
puts "map (#{label})"
|
61
|
+
3.times do
|
62
|
+
bm.report do
|
63
|
+
times.times do
|
64
|
+
coll.map {|n| n + 1}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
puts "map (#{label})"
|
70
|
+
3.times do
|
71
|
+
bm.report do
|
72
|
+
times.times do
|
73
|
+
T.transduce(map_inc, :<<, [], coll)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
puts
|
79
|
+
|
80
|
+
puts "map + select (#{label})"
|
81
|
+
3.times do
|
82
|
+
bm.report do
|
83
|
+
times.times do
|
84
|
+
coll.
|
85
|
+
map {|n| n + 1}.
|
86
|
+
select {|n| n.even?}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
puts "map + filter (#{label})"
|
92
|
+
3.times do
|
93
|
+
bm.report do
|
94
|
+
times.times do
|
95
|
+
T.transduce(map_inc_filter_even, :<<, [], coll)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
puts
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
__END__
|
105
|
+
|
106
|
+
select (enum)
|
107
|
+
0.010000 0.000000 0.010000 ( 0.008299)
|
108
|
+
0.010000 0.000000 0.010000 ( 0.007843)
|
109
|
+
0.000000 0.000000 0.000000 ( 0.007821)
|
110
|
+
filter (enum)
|
111
|
+
0.020000 0.000000 0.020000 ( 0.017747)
|
112
|
+
0.020000 0.000000 0.020000 ( 0.019915)
|
113
|
+
0.020000 0.000000 0.020000 ( 0.019416)
|
114
|
+
|
115
|
+
map (enum)
|
116
|
+
0.010000 0.000000 0.010000 ( 0.008053)
|
117
|
+
0.010000 0.000000 0.010000 ( 0.007952)
|
118
|
+
0.010000 0.000000 0.010000 ( 0.009550)
|
119
|
+
map (enum)
|
120
|
+
0.020000 0.000000 0.020000 ( 0.021479)
|
121
|
+
0.020000 0.000000 0.020000 ( 0.020805)
|
122
|
+
0.020000 0.000000 0.020000 ( 0.023205)
|
123
|
+
|
124
|
+
map + select (enum)
|
125
|
+
0.010000 0.000000 0.010000 ( 0.015156)
|
126
|
+
0.020000 0.000000 0.020000 ( 0.016545)
|
127
|
+
0.010000 0.010000 0.020000 ( 0.019388)
|
128
|
+
map + filter (enum)
|
129
|
+
0.030000 0.000000 0.030000 ( 0.026530)
|
130
|
+
0.030000 0.000000 0.030000 ( 0.025563)
|
131
|
+
0.020000 0.000000 0.020000 ( 0.027893)
|
132
|
+
|
133
|
+
select (array)
|
134
|
+
0.010000 0.000000 0.010000 ( 0.006520)
|
135
|
+
0.010000 0.000000 0.010000 ( 0.006570)
|
136
|
+
0.000000 0.000000 0.000000 ( 0.007032)
|
137
|
+
filter (array)
|
138
|
+
0.020000 0.000000 0.020000 ( 0.022639)
|
139
|
+
0.030000 0.000000 0.030000 ( 0.023813)
|
140
|
+
0.020000 0.000000 0.020000 ( 0.022440)
|
141
|
+
|
142
|
+
map (array)
|
143
|
+
0.010000 0.000000 0.010000 ( 0.005880)
|
144
|
+
0.000000 0.000000 0.000000 ( 0.005613)
|
145
|
+
0.010000 0.000000 0.010000 ( 0.005294)
|
146
|
+
map (array)
|
147
|
+
0.020000 0.000000 0.020000 ( 0.024443)
|
148
|
+
0.030000 0.000000 0.030000 ( 0.023856)
|
149
|
+
0.020000 0.000000 0.020000 ( 0.024172)
|
150
|
+
|
151
|
+
map + select (array)
|
152
|
+
0.010000 0.000000 0.010000 ( 0.012158)
|
153
|
+
0.010000 0.000000 0.010000 ( 0.012061)
|
154
|
+
0.020000 0.000000 0.020000 ( 0.014131)
|
155
|
+
map + filter (array)
|
156
|
+
0.020000 0.000000 0.020000 ( 0.026898)
|
157
|
+
0.030000 0.000000 0.030000 ( 0.025745)
|
158
|
+
0.030000 0.000000 0.030000 ( 0.028196)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env sh
|
2
|
+
# Copyright (c) Cognitect, Inc.
|
3
|
+
# All rights reserved.
|
4
|
+
|
5
|
+
for r in 1.9.3-p547 2.0.0-p353 2.1.0 2.1.1 2.1.2 jruby-1.7.13 jruby-1.7.14 jruby-1.7.15 jruby-1.7.16
|
6
|
+
do
|
7
|
+
eval "rbenv local $r"
|
8
|
+
echo `ruby -v`
|
9
|
+
rspec
|
10
|
+
done
|
11
|
+
|
12
|
+
rm .ruby-version
|
data/build/revision
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
# Returns the revision number used for deployment.
|
4
|
+
|
5
|
+
set -e
|
6
|
+
|
7
|
+
REVISION=`git --no-replace-objects describe --tags --match v0.0`
|
8
|
+
|
9
|
+
# Extract the version number from the string. Do this in two steps so
|
10
|
+
# it is a little easier to understand.
|
11
|
+
REVISION=${REVISION:5} # drop the first 5 characters
|
12
|
+
REVISION=${REVISION:0:${#REVISION}-9} # drop the last 9 characters
|
13
|
+
|
14
|
+
echo $REVISION
|
data/dev/irb_tools.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# Copyright 2014 Cognitect. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'stringio'
|
16
|
+
|
17
|
+
def time
|
18
|
+
start = Time.now
|
19
|
+
yield
|
20
|
+
puts "Elapsed: #{Time.now - start}"
|
21
|
+
end
|
22
|
+
|
23
|
+
class Object
|
24
|
+
def to_transit(format=:json)
|
25
|
+
sio = StringIO.new
|
26
|
+
Transit::Writer.new(format, sio).write(self)
|
27
|
+
sio.string
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class String
|
32
|
+
def from_transit(format=:json)
|
33
|
+
sio = StringIO.new(self)
|
34
|
+
Transit::Reader.new(format, sio).read
|
35
|
+
end
|
36
|
+
end
|
data/lib/transducers.rb
ADDED
@@ -0,0 +1,615 @@
|
|
1
|
+
# Copyright 2014 Cognitect. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS-IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
# Transducers are composable algorithmic transformations. See
|
16
|
+
# http://clojure.org/transducers before reading on.
|
17
|
+
#
|
18
|
+
# ## Terminology
|
19
|
+
#
|
20
|
+
# We need to expand the terminology a bit in order to map the concepts
|
21
|
+
# described on http://clojure.org/transducers to an OO language like
|
22
|
+
# Ruby.
|
23
|
+
#
|
24
|
+
# A _reducer_ is an object with a `step` method that takes a result
|
25
|
+
# (so far) and an input and returns a new result. This is similar to
|
26
|
+
# the blocks we pass to Ruby's `reduce` (a.k.a `inject`), and serves a
|
27
|
+
# similar role in _transducing process_.
|
28
|
+
#
|
29
|
+
# A _handler_ is an object with a `call` method that a reducer uses
|
30
|
+
# to process input. In a `map` operation, this would transform the
|
31
|
+
# input, and in a `filter` operation it would act as a predicate.
|
32
|
+
#
|
33
|
+
# A _transducer_ is an object that transforms a reducer by adding
|
34
|
+
# additional processing for each element in a collection of inputs.
|
35
|
+
#
|
36
|
+
# A _transducing process_ is invoked by calling
|
37
|
+
# `Transducers.transduce` with a transducer, a reducer, an optional
|
38
|
+
# initial value, and an input collection.
|
39
|
+
#
|
40
|
+
# Because Ruby doesn't come with free-floating handlers (e.g. Clojure's
|
41
|
+
# `inc` function) or reducing functions (e.g. Clojure's `conj`), we have
|
42
|
+
# to build these things ourselves.
|
43
|
+
#
|
44
|
+
# ## Examples
|
45
|
+
#
|
46
|
+
# ```ruby
|
47
|
+
# # handler
|
48
|
+
# inc = Class.new do
|
49
|
+
# def call(input) input += 1 end
|
50
|
+
# end.new
|
51
|
+
#
|
52
|
+
# # reducer
|
53
|
+
# appender = Class.new do
|
54
|
+
# def step(result, input) result << input end
|
55
|
+
# end.new
|
56
|
+
#
|
57
|
+
# # transducing process
|
58
|
+
# Transducers.transduce(Transducers.map(inc), appender, [], 0..9)
|
59
|
+
# #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
60
|
+
# ```
|
61
|
+
#
|
62
|
+
# You can pass a `Symbol` or a `Block` to transducer constructors
|
63
|
+
# (`Transducers.map` in this example), so the above can be achieved
|
64
|
+
# more easily e.g.
|
65
|
+
#
|
66
|
+
# ```
|
67
|
+
# Transducers.transduce(Transducers.map(:succ), appender, [], 0..9)
|
68
|
+
# #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
69
|
+
# Transducers.transduce(Transducers.map {|n|n+1}, appender, [], 0..9)
|
70
|
+
# #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
71
|
+
# ```
|
72
|
+
#
|
73
|
+
# You can omit the initial value if the reducer (`appender` in this
|
74
|
+
# example) provides one:
|
75
|
+
#
|
76
|
+
# ```
|
77
|
+
# def appender.init() [] end
|
78
|
+
# Transducers.transduce(Transducers.map {|n|n+1}, appender, 0..9)
|
79
|
+
# #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
80
|
+
# ```
|
81
|
+
#
|
82
|
+
# You can also just pass a `Symbol` and an initial value instead of a
|
83
|
+
# reducer object, and the `transduce` method will build one for you.
|
84
|
+
#
|
85
|
+
# ```
|
86
|
+
# Transducers.transduce(Transducers.map {|n|n+1}, :<<, [], 0..9)
|
87
|
+
# #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
88
|
+
# ```
|
89
|
+
#
|
90
|
+
# ## Composition
|
91
|
+
#
|
92
|
+
# Imagine that you want to take a range of numbers, select all the even
|
93
|
+
# ones, double them, and then take the first 5. Here's one way to
|
94
|
+
# do that in Ruby:
|
95
|
+
#
|
96
|
+
# ```ruby
|
97
|
+
# (1..100).
|
98
|
+
# select {|n| n.even?}.
|
99
|
+
# map {|n| n * 2}.
|
100
|
+
# take(5)
|
101
|
+
# #=> [4, 8, 12, 16, 20]
|
102
|
+
# ```
|
103
|
+
#
|
104
|
+
# Here's the same process with transducers:
|
105
|
+
#
|
106
|
+
# ```ruby
|
107
|
+
# t = Transducers.compose(
|
108
|
+
# Transducers.filter(:even?),
|
109
|
+
# Transducers.map {|n| n * 2},
|
110
|
+
# Transducers.take(5))
|
111
|
+
# Transducers.transduce(t, :<<, [], 1..100)
|
112
|
+
# #=> [4, 8, 12, 16, 20]
|
113
|
+
# ```
|
114
|
+
#
|
115
|
+
# Now that we've defined the transducer as a series of
|
116
|
+
# transformations, we can apply it to different contexts, e.g.
|
117
|
+
#
|
118
|
+
# ```ruby
|
119
|
+
# Transducers.transduce(t, :+, 0, 1..100)
|
120
|
+
# #=> 60
|
121
|
+
# Transducers.transduce(t, :*, 1, 1..100)
|
122
|
+
# #=> 122880
|
123
|
+
# ```
|
124
|
+
module Transducers
|
125
|
+
class Reducer
|
126
|
+
def initialize(init, sym=nil, &block)
|
127
|
+
raise ArgumentError.new("No init provided") if init == :no_init_provided
|
128
|
+
@init = init
|
129
|
+
if sym
|
130
|
+
@sym = sym
|
131
|
+
(class << self; self; end).class_eval do
|
132
|
+
def step(result, input)
|
133
|
+
result.send(@sym, input)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
else
|
137
|
+
@block = block
|
138
|
+
(class << self; self; end).class_eval do
|
139
|
+
def step(result, input)
|
140
|
+
@block.call(result, input)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def init() @init end
|
147
|
+
def complete(result) result end
|
148
|
+
def step(result, input)
|
149
|
+
# placeholder for docs - overwritten in initalize
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# @api private
|
154
|
+
class Reduced
|
155
|
+
attr_reader :val
|
156
|
+
|
157
|
+
def initialize(val)
|
158
|
+
@val = val
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# @api private
|
163
|
+
class PreservingReduced
|
164
|
+
def apply(reducer)
|
165
|
+
@reducer = reducer
|
166
|
+
end
|
167
|
+
|
168
|
+
def step(result, input)
|
169
|
+
ret = @reducer.step(result, input)
|
170
|
+
Reduced === ret ? Reduced.new(ret) : ret
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class WrappingReducer
|
175
|
+
class MethodHandler
|
176
|
+
def initialize(method)
|
177
|
+
@method = method
|
178
|
+
end
|
179
|
+
|
180
|
+
def call(input)
|
181
|
+
input.send @method
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def initialize(reducer, handler=nil, &block)
|
186
|
+
@reducer = reducer
|
187
|
+
@handler = if block
|
188
|
+
block
|
189
|
+
elsif Symbol === handler
|
190
|
+
MethodHandler.new(handler)
|
191
|
+
else
|
192
|
+
handler
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def init()
|
197
|
+
@reducer.init
|
198
|
+
end
|
199
|
+
|
200
|
+
def complete(result)
|
201
|
+
@reducer.complete(result)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# @api private
|
206
|
+
class BaseTransducer
|
207
|
+
class << self
|
208
|
+
attr_reader :reducer_class
|
209
|
+
|
210
|
+
def define_reducer_class(&block)
|
211
|
+
@reducer_class = Class.new(WrappingReducer)
|
212
|
+
@reducer_class.class_eval(&block)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def initialize(handler, &block)
|
217
|
+
@handler = handler
|
218
|
+
@block = block
|
219
|
+
end
|
220
|
+
|
221
|
+
def reducer_class
|
222
|
+
self.class.reducer_class
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
class << self
|
227
|
+
# @overload transduce(transducer, reducer, coll)
|
228
|
+
# @overload transduce(transducer, reducer, init, coll)
|
229
|
+
# @param [Transducer] transducer
|
230
|
+
# @param [Reducer, Symbol, Bock] reducer
|
231
|
+
def transduce(transducer, reducer, init=:no_init_provided, coll)
|
232
|
+
reducer = Reducer.new(init, reducer) unless reducer.respond_to?(:step)
|
233
|
+
reducer = transducer.apply(reducer)
|
234
|
+
result = init == :no_init_provided ? reducer.init : init
|
235
|
+
case coll
|
236
|
+
when Enumerable
|
237
|
+
coll.each do |input|
|
238
|
+
result = reducer.step(result, input)
|
239
|
+
return result.val if Transducers::Reduced === result
|
240
|
+
result
|
241
|
+
end
|
242
|
+
when String
|
243
|
+
coll.each_char do |input|
|
244
|
+
result = reducer.step(result, input)
|
245
|
+
return result.val if Transducers::Reduced === result
|
246
|
+
result
|
247
|
+
end
|
248
|
+
end
|
249
|
+
reducer.complete(result)
|
250
|
+
end
|
251
|
+
|
252
|
+
def self.define_transducer_class(name, &block)
|
253
|
+
t = Class.new(BaseTransducer)
|
254
|
+
t.class_eval(&block)
|
255
|
+
unless t.instance_methods.include? :apply
|
256
|
+
t.class_eval do
|
257
|
+
define_method :apply do |reducer|
|
258
|
+
reducer_class.new(reducer, @handler, &@block)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
Transducers.send(:define_method, name) do |handler=nil, &b|
|
264
|
+
t.new(handler, &b)
|
265
|
+
end
|
266
|
+
|
267
|
+
Transducers.send(:module_function, name)
|
268
|
+
end
|
269
|
+
|
270
|
+
# @macro [new] common_transducer
|
271
|
+
# @return [Transducer]
|
272
|
+
# @method $1(handler=nil, &block)
|
273
|
+
# @param [Object, Symbol] handler
|
274
|
+
# Given an object that responds to +process+, uses it as the
|
275
|
+
# handler. Given a +Symbol+, builds a handler whose +process+
|
276
|
+
# method will send +Symbol+ to its argument.
|
277
|
+
# @param [Block] block <i>(optional)</i>
|
278
|
+
# Given a +Block+, builds a handler whose +process+ method will
|
279
|
+
# call the block with its argument(s).
|
280
|
+
# Returns a transducer that adds a map transformation to the
|
281
|
+
# reducer stack.
|
282
|
+
define_transducer_class :map do
|
283
|
+
define_reducer_class do
|
284
|
+
# Can I doc this?
|
285
|
+
def step(result, input)
|
286
|
+
@reducer.step(result, @handler.call(input))
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# @macro common_transducer
|
292
|
+
define_transducer_class :filter do
|
293
|
+
define_reducer_class do
|
294
|
+
def step(result, input)
|
295
|
+
@handler.call(input) ? @reducer.step(result, input) : result
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# @macro common_transducer
|
301
|
+
define_transducer_class :remove do
|
302
|
+
define_reducer_class do
|
303
|
+
def step(result, input)
|
304
|
+
@handler.call(input) ? result : @reducer.step(result, input)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
# @method take(n)
|
310
|
+
# @return [Transducer]
|
311
|
+
define_transducer_class :take do
|
312
|
+
define_reducer_class do
|
313
|
+
def initialize(reducer, n)
|
314
|
+
super(reducer)
|
315
|
+
@n = n
|
316
|
+
end
|
317
|
+
|
318
|
+
def step(result, input)
|
319
|
+
@n -= 1
|
320
|
+
ret = @reducer.step(result, input)
|
321
|
+
@n > 0 ? ret : Reduced.new(ret)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def initialize(n)
|
326
|
+
@n = n
|
327
|
+
end
|
328
|
+
|
329
|
+
def apply(reducer)
|
330
|
+
reducer_class.new(reducer, @n)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# @macro common_transducer
|
335
|
+
define_transducer_class :take_while do
|
336
|
+
define_reducer_class do
|
337
|
+
def step(result, input)
|
338
|
+
@handler.call(input) ? @reducer.step(result, input) : Reduced.new(result)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
# @method take_nth(n)
|
344
|
+
# @return [Transducer]
|
345
|
+
define_transducer_class :take_nth do
|
346
|
+
define_reducer_class do
|
347
|
+
def initialize(reducer, n)
|
348
|
+
super(reducer)
|
349
|
+
@n = n
|
350
|
+
@count = 0
|
351
|
+
end
|
352
|
+
|
353
|
+
def step(result, input)
|
354
|
+
@count += 1
|
355
|
+
if @count % @n == 0
|
356
|
+
@reducer.step(result, input)
|
357
|
+
else
|
358
|
+
result
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def initialize(n)
|
364
|
+
@n = n
|
365
|
+
end
|
366
|
+
|
367
|
+
def apply(reducer)
|
368
|
+
reducer_class.new(reducer, @n)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# @method replace(source_map)
|
373
|
+
# @return [Transducer]
|
374
|
+
define_transducer_class :replace do
|
375
|
+
define_reducer_class do
|
376
|
+
def initialize(reducer, smap)
|
377
|
+
super(reducer)
|
378
|
+
@smap = smap
|
379
|
+
end
|
380
|
+
|
381
|
+
def step(result, input)
|
382
|
+
if @smap.has_key?(input)
|
383
|
+
@reducer.step(result, @smap[input])
|
384
|
+
else
|
385
|
+
@reducer.step(result, input)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def initialize(smap)
|
391
|
+
@smap = case smap
|
392
|
+
when Hash
|
393
|
+
smap
|
394
|
+
else
|
395
|
+
(0...smap.size).zip(smap).to_h
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
def apply(reducer)
|
400
|
+
reducer_class.new(reducer, @smap)
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
# @macro common_transducer
|
405
|
+
define_transducer_class :keep do
|
406
|
+
define_reducer_class do
|
407
|
+
def step(result, input)
|
408
|
+
x = @handler.call(input)
|
409
|
+
x.nil? ? result : @reducer.step(result, x)
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
# @macro common_transducer
|
415
|
+
# @note the handler for this method requires two arguments: the
|
416
|
+
# index and the input.
|
417
|
+
define_transducer_class :keep_indexed do
|
418
|
+
define_reducer_class do
|
419
|
+
def initialize(*)
|
420
|
+
super
|
421
|
+
@index = -1
|
422
|
+
end
|
423
|
+
|
424
|
+
def step(result, input)
|
425
|
+
@index += 1
|
426
|
+
x = @handler.call(@index, input)
|
427
|
+
x.nil? ? result : @reducer.step(result, x)
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
# @method drop(n)
|
433
|
+
# @return [Transducer]
|
434
|
+
define_transducer_class :drop do
|
435
|
+
define_reducer_class do
|
436
|
+
def initialize(reducer, n)
|
437
|
+
super(reducer)
|
438
|
+
@n = n
|
439
|
+
end
|
440
|
+
|
441
|
+
def step(result, input)
|
442
|
+
@n -= 1
|
443
|
+
@n <= -1 ? @reducer.step(result, input) : result
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
def initialize(n)
|
448
|
+
@n = n
|
449
|
+
end
|
450
|
+
|
451
|
+
def apply(reducer)
|
452
|
+
reducer_class.new(reducer, @n)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
# @macro common_transducer
|
457
|
+
define_transducer_class :drop_while do
|
458
|
+
define_reducer_class do
|
459
|
+
def initalize(*)
|
460
|
+
super
|
461
|
+
@done_dropping = false
|
462
|
+
end
|
463
|
+
|
464
|
+
def step(result, input)
|
465
|
+
@done_dropping ||= !@handler.call(input)
|
466
|
+
@done_dropping ? @reducer.step(result, input) : result
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
# @method dedupe
|
472
|
+
# @return [Transducer]
|
473
|
+
define_transducer_class :dedupe do
|
474
|
+
define_reducer_class do
|
475
|
+
def initialize(*)
|
476
|
+
super
|
477
|
+
@prior = :no_value_provided_for_transducer
|
478
|
+
end
|
479
|
+
|
480
|
+
def step(result, input)
|
481
|
+
ret = input == @prior ? result : @reducer.step(result, input)
|
482
|
+
@prior = input
|
483
|
+
ret
|
484
|
+
end
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
# @method partition_by
|
489
|
+
# @return [Transducer]
|
490
|
+
define_transducer_class :partition_by do
|
491
|
+
define_reducer_class do
|
492
|
+
def initialize(*)
|
493
|
+
super
|
494
|
+
@a = []
|
495
|
+
@prev_val = :no_value_provided_for_transducer
|
496
|
+
end
|
497
|
+
|
498
|
+
def complete(result)
|
499
|
+
result = if @a.empty?
|
500
|
+
result
|
501
|
+
else
|
502
|
+
a = @a.dup
|
503
|
+
@a.clear
|
504
|
+
@reducer.step(result, a)
|
505
|
+
end
|
506
|
+
@reducer.complete(result)
|
507
|
+
end
|
508
|
+
|
509
|
+
def step(result, input)
|
510
|
+
prev_val = @prev_val
|
511
|
+
val = @handler.call(input)
|
512
|
+
@prev_val = val
|
513
|
+
if val == prev_val || prev_val == :no_value_provided_for_transducer
|
514
|
+
@a << input
|
515
|
+
result
|
516
|
+
else
|
517
|
+
a = @a.dup
|
518
|
+
@a.clear
|
519
|
+
ret = @reducer.step(result, a)
|
520
|
+
@a << input unless (Reduced === ret)
|
521
|
+
ret
|
522
|
+
end
|
523
|
+
end
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
# @method partition_all
|
528
|
+
# @return [Transducer]
|
529
|
+
define_transducer_class :partition_all do
|
530
|
+
define_reducer_class do
|
531
|
+
def initialize(reducer, n)
|
532
|
+
super(reducer)
|
533
|
+
@n = n
|
534
|
+
@a = []
|
535
|
+
end
|
536
|
+
|
537
|
+
def step(result, input)
|
538
|
+
@a << input
|
539
|
+
if @a.size == @n
|
540
|
+
a = @a.dup
|
541
|
+
@a.clear
|
542
|
+
@reducer.step(result, a)
|
543
|
+
else
|
544
|
+
result
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
def complete(result)
|
549
|
+
if @a.empty?
|
550
|
+
result
|
551
|
+
else
|
552
|
+
a = @a.dup
|
553
|
+
@a.clear
|
554
|
+
@reducer.step(result, a)
|
555
|
+
end
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
def initialize(n)
|
560
|
+
@n = n
|
561
|
+
end
|
562
|
+
|
563
|
+
def apply(reducer)
|
564
|
+
reducer_class.new(reducer, @n)
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
# @api private
|
569
|
+
class RandomSampleHandler
|
570
|
+
def initialize(prob)
|
571
|
+
@prob = prob
|
572
|
+
end
|
573
|
+
|
574
|
+
def call(_)
|
575
|
+
@prob > Random.rand
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
# @return [Transducer]
|
580
|
+
def random_sample(prob)
|
581
|
+
filter RandomSampleHandler.new(prob)
|
582
|
+
end
|
583
|
+
|
584
|
+
# @method cat
|
585
|
+
# @return [Transducer]
|
586
|
+
define_transducer_class :cat do
|
587
|
+
define_reducer_class do
|
588
|
+
def step(result, input)
|
589
|
+
Transducers.transduce(PreservingReduced.new, @reducer, result, input)
|
590
|
+
end
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
# @api private
|
595
|
+
class ComposedTransducer
|
596
|
+
def initialize(*transducers)
|
597
|
+
@transducers = transducers
|
598
|
+
end
|
599
|
+
|
600
|
+
def apply(reducer)
|
601
|
+
@transducers.reverse.reduce(reducer) {|r,t| t.apply(r)}
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
# @return [Transducer]
|
606
|
+
def compose(*transducers)
|
607
|
+
ComposedTransducer.new(*transducers)
|
608
|
+
end
|
609
|
+
|
610
|
+
# @return [Transducer]
|
611
|
+
def mapcat(handler=nil, &block)
|
612
|
+
compose(map(handler, &block), cat)
|
613
|
+
end
|
614
|
+
end
|
615
|
+
end
|