transducers 0.4.94
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/.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
|