coroutines 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.rdoc +150 -0
- data/lib/coroutines/base.rb +329 -0
- data/lib/coroutines/sink.rb +149 -0
- data/lib/coroutines.rb +334 -0
- data/tests/test_coroutines.rb +154 -0
- metadata +59 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 89eb0428ee0c27d848aff55a5cbe954edd6f221b
|
4
|
+
data.tar.gz: 4b34bc8df721b4923ba524174dbe396957d75b8e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 26b3c54acdf282c3db224f3672092bb1f2dcbd7f65d8ac461fa55daec860002fb90e6438f4b6f7dac1143759888616df3b8b2ffb523bf1cc61c02a0a910020f6
|
7
|
+
data.tar.gz: a1f1e7a915922b2ef9e81bcf84600003679deb879fbe56a9a45142bce74e39c96f2d550e8a65a7c8e59248518d77f3554434d1e2bf3bd4652d6b17954e4489b9
|
data/README.rdoc
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
= Coroutines
|
2
|
+
A library for creating and composing producer/transformer/consumer coroutines.
|
3
|
+
Producers are already provided by Ruby's built-in Enumerator class; this
|
4
|
+
library provides Transformer and Consumer classes that work analogously. In
|
5
|
+
particular, they are also based on Fiber and not on threads (as in some other
|
6
|
+
producer/consumer libraries). Also provides a module Sink, which is analogous
|
7
|
+
to Enumerable, and Enumerable/Transformer/Sink composition using overloaded
|
8
|
+
<= and >= operators.
|
9
|
+
|
10
|
+
== Installing
|
11
|
+
gem install coroutines
|
12
|
+
|
13
|
+
== Using
|
14
|
+
A simple consumer:
|
15
|
+
|
16
|
+
require 'coroutines'
|
17
|
+
|
18
|
+
def counter(start)
|
19
|
+
result = start
|
20
|
+
loop { result += await }
|
21
|
+
"Final value: #{result}"
|
22
|
+
end
|
23
|
+
|
24
|
+
co = consum_for :counter, 10 # => #<Consumer: main:counter (running)>
|
25
|
+
co << 10 << 1000 << 10000
|
26
|
+
co.close # => "Final value: 11020"
|
27
|
+
|
28
|
+
The call to Consumer#close raises StopIteration at the point at which the consumer
|
29
|
+
last executed await. In this case, the StopIteration is caught by loop,
|
30
|
+
causing it to terminate.
|
31
|
+
|
32
|
+
Note that this is an intentionally simplistic example intended to show the
|
33
|
+
basic library API. Of course, a counter could just as easily be implemented
|
34
|
+
using a closure; the advantage of a consumer is that the implementation could
|
35
|
+
involve arbitrary control structures with multiple calls to await.
|
36
|
+
|
37
|
+
A simple transformer:
|
38
|
+
|
39
|
+
require 'coroutines'
|
40
|
+
|
41
|
+
def running_sum(start)
|
42
|
+
result = start
|
43
|
+
loop { result += await; yield result }
|
44
|
+
end
|
45
|
+
|
46
|
+
tr = trans_for :running_sum, 3 # => #<Transformer: main:running_sum>
|
47
|
+
sums = (1..10) >= tr # => #<Enumerator: #<Transformer: main:running_sum> <= 1..10>
|
48
|
+
sums.to_a # => [4, 6, 9, 13, 18, 24, 31, 39, 48, 58]
|
49
|
+
|
50
|
+
tr = trans_for :running_sum, 0 # => #<Transformer: main:running_sum>
|
51
|
+
collect_sums = tr >= []
|
52
|
+
collect_sums << 1 << 1 << 2 << 3 << 5
|
53
|
+
collect_sums.close # => [1, 2, 4, 7, 12]
|
54
|
+
|
55
|
+
Again, this is just a basic demonstration of the API that could be written
|
56
|
+
without resorting to coroutines (though probably not quite as succinctly).
|
57
|
+
|
58
|
+
== Sources and sinks
|
59
|
+
As you probably know, many Ruby classes use the Enumerable mixin to provide
|
60
|
+
common functionality like mapping and filtering of sequences of values. We can
|
61
|
+
think of such classes as "sources" of the values yielded by their respective
|
62
|
+
each methods. In the same way, several classes use the << operator to
|
63
|
+
_accept_ sequences of values:
|
64
|
+
|
65
|
+
$stdout << "a" << "b" << "c" # prints "abc"
|
66
|
+
[] << "a" << "b" << "c" # => ["a", "b", "c"]
|
67
|
+
"" << "a" << "b" << "c" # => "abc"
|
68
|
+
|
69
|
+
The +coroutines+ library provides the mixin Sink for such classes. Among
|
70
|
+
other methods, this provides an operator <= for "connecting" sources (i.e.
|
71
|
+
enumerables) to sinks.
|
72
|
+
|
73
|
+
open("test.txt", "w") <= ["a", "b", "c"] # write "abc" to test.txt
|
74
|
+
["a", "b", "c"] <= ("d".."f") # => ["a", "b", "c", "d", "e", "f"]
|
75
|
+
"abc" <= ("d".."f") # => "abcdef"
|
76
|
+
|
77
|
+
Note that <= closes the sink after having exhausted the enumerable,
|
78
|
+
returning whatever #close returns. close defaults to simply returning the
|
79
|
+
sink itself, as in the Array and String examples above. For IO, which has
|
80
|
+
its own close implementation, this implies that the file descriptor will be
|
81
|
+
closed after the <= operation finishes. If this is not what you want, use
|
82
|
+
dup:
|
83
|
+
|
84
|
+
$stdout.dup <= ["a", "b", "c"] # print "abc"
|
85
|
+
|
86
|
+
For symmetry, the coroutines library augments Enumerable with an operator
|
87
|
+
>= that mirrors <=:
|
88
|
+
|
89
|
+
("d".."f") >= "abc" # => "abcdef"
|
90
|
+
|
91
|
+
== Pipelines involving transformers
|
92
|
+
Re-using the running_sum example from above:
|
93
|
+
|
94
|
+
(1..10) >= trans_for(:running_sum, 0) >= proc{|x| x.to_s + "\n" } >= $stdout.dup
|
95
|
+
|
96
|
+
What does this do? Well, it takes the sequences of integers from 1 to 10, then
|
97
|
+
computes the running sum, then convers each partial sum to a string, and
|
98
|
+
finally prints out each string to $stdout. Except that the "thens" in the
|
99
|
+
previous sentence are not entirely correct, since the processing stages run in
|
100
|
+
parallel (using coroutines, so blocking IO in one stage will block all other
|
101
|
+
stages). Instead of (1..10), we could have a File and iterate over GBs of data,
|
102
|
+
and at no point would we need to have the entire sequence in memory.
|
103
|
+
|
104
|
+
Any part of a pipeline can be passed around and stored as an individual object:
|
105
|
+
|
106
|
+
enum = (1..10) >= trans_for(:running_sum, 0) >= proc{|x| x.to_s + "\n" } # => an Enumerator
|
107
|
+
cons = trans_for(:running_sum, 0) >= proc{|x| x.to_s + "\n" } >= $stdout.dup # => a Consumer
|
108
|
+
trans = trans_for(:running_sum, 0) >= proc{|x| x.to_s + "\n" } # => a Transformer
|
109
|
+
|
110
|
+
== map/filter equivalent pipelines
|
111
|
+
As shown above, a Proc object can be used in place of a Transformer. Connecting
|
112
|
+
transformer procs to enums, sinks or other procs is special-cased to minimize
|
113
|
+
overhead, so that something like
|
114
|
+
|
115
|
+
(1..9) >= proc{|x| x.to_s if x.even? } >= ""
|
116
|
+
|
117
|
+
is just syntactic sugar for
|
118
|
+
|
119
|
+
(1..9).lazy.select{|x| x.even? }.map{|x| x.to_s }.inject(""){|memo, x| memo << x }
|
120
|
+
|
121
|
+
(And yes, the latter could also be written more succinctly if Enumerator::Lazy
|
122
|
+
would provide operations like filter_map and join.)
|
123
|
+
|
124
|
+
== Links and Acknowledgements
|
125
|
+
Inspired by {David M. Beazley's Generator Tricks}[http://www.dabeaz.com/generators] (Python)
|
126
|
+
and {Michael Snoyman's conduit package}[https://www.fpcomplete.com/user/snoyberg/library-documentation/conduit-overview] (Haskell).
|
127
|
+
|
128
|
+
== Compatibility
|
129
|
+
Tested with MRI 1.9.3p484, 2.0.0p384 and 2.2.0dev (trunk 47827) on Linux.
|
130
|
+
Should work on other moderately recent versions and other operating systems.
|
131
|
+
Will probably not work on all alternative Ruby implementations, because it
|
132
|
+
depends on the fiber library.
|
133
|
+
|
134
|
+
Requiring 'coroutines' does some monkey patching to various core classes, which
|
135
|
+
may be a compatibility issue. It is possible to load just the core module
|
136
|
+
( Sink) and classes ( Consumer and Transformer) using
|
137
|
+
"require 'coroutines/base'". Obviously, you will then have to instantiate
|
138
|
+
Consumer and Transformer manually in order to use them; and you'll have to be
|
139
|
+
more explicit when constructing certain pipelines.
|
140
|
+
|
141
|
+
Patched core modules and classes are:
|
142
|
+
Enumerable:: add Enumerable#>= operator
|
143
|
+
IO, Array, String:: include Sink mixin
|
144
|
+
Hash:: define Hash#<< operator and include Sink mixin
|
145
|
+
Method:: define Method#<< operator, define Method#close as an alias
|
146
|
+
for Method#receiver, and include Sink mixin
|
147
|
+
Object:: define Object#await, Object#consum_for and Object#trans_for
|
148
|
+
Proc:: define Proc#to_trans, Proc#<= and Proc#>=
|
149
|
+
Symbol:: define Symbol#to_trans, Symbol#<= and Symbol#>=
|
150
|
+
|
@@ -0,0 +1,329 @@
|
|
1
|
+
require 'fiber'
|
2
|
+
require 'coroutines/sink'
|
3
|
+
|
4
|
+
# A class implementing consumer coroutines
|
5
|
+
#
|
6
|
+
# A Consumer can be created by the following methods:
|
7
|
+
# * Object#consum_for
|
8
|
+
# * Object#consumer
|
9
|
+
# * Consumer.new
|
10
|
+
# * Transformer#>=
|
11
|
+
#
|
12
|
+
# See Object#consum_for for an explanation of the basic concepts.
|
13
|
+
class Consumer
|
14
|
+
class Yielder
|
15
|
+
if Fiber.instance_methods.include? :raise
|
16
|
+
def await
|
17
|
+
Fiber.yield
|
18
|
+
end
|
19
|
+
else
|
20
|
+
@@exception_wrapper = Class.new do
|
21
|
+
define_method :initialize do |*args|
|
22
|
+
@args = args
|
23
|
+
end
|
24
|
+
attr_reader :args
|
25
|
+
end
|
26
|
+
|
27
|
+
def await
|
28
|
+
item = Fiber.yield
|
29
|
+
raise(*item.args) if item.instance_of? @@exception_wrapper
|
30
|
+
return item
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# :call-seq:
|
36
|
+
# Consumer.new { |yielder| ... } -> consumer
|
37
|
+
#
|
38
|
+
# Creates a new Consumer coroutine, which can be used as a Sink.
|
39
|
+
#
|
40
|
+
# The block is called immediately with a "yielder" object as parameter.
|
41
|
+
# +yielder+ can be used to retrieve a value from the consumption context by
|
42
|
+
# calling its await method.
|
43
|
+
#
|
44
|
+
# consum = Consumer.new do |y|
|
45
|
+
# a = y.await
|
46
|
+
# b = y.await
|
47
|
+
# "#{a} + #{b} = #{a + b}"
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# consum << 42 << 24
|
51
|
+
# consum.close # => "42 + 24 = 66"
|
52
|
+
#
|
53
|
+
def initialize(&block)
|
54
|
+
@fiber = Fiber.new(&block)
|
55
|
+
@result = nil
|
56
|
+
|
57
|
+
self << Yielder.new
|
58
|
+
end
|
59
|
+
|
60
|
+
def inspect
|
61
|
+
status = if @fiber.alive? then "running" else @result.inspect end
|
62
|
+
"#<Consumer: 0x#{object_id.to_s(16)} (#{status})>"
|
63
|
+
end
|
64
|
+
|
65
|
+
# After the consumer has terminated (which may have happened in response to
|
66
|
+
# Consumer#close), result contains the value returned by the consumer.
|
67
|
+
attr_reader :result
|
68
|
+
|
69
|
+
# :call-seq:
|
70
|
+
# consumer << obj -> consumer
|
71
|
+
#
|
72
|
+
# Feeds +obj+ as an input to the consumer.
|
73
|
+
def <<(obj)
|
74
|
+
raise StopIteration unless @fiber.alive?
|
75
|
+
value = @fiber.resume(obj)
|
76
|
+
@result = value unless @fiber.alive?
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
if Fiber.instance_methods.include? :raise
|
81
|
+
# :call-seq:
|
82
|
+
# consumer.close -> obj
|
83
|
+
#
|
84
|
+
# Terminate the consumer by raising StopIteration at the point where it
|
85
|
+
# last requested an input value. +obj+ is the return value of the consumer.
|
86
|
+
def close
|
87
|
+
@result = @fiber.raise StopIteration if @fiber.alive?
|
88
|
+
return @result
|
89
|
+
rescue StopIteration
|
90
|
+
return @result
|
91
|
+
end
|
92
|
+
else
|
93
|
+
def close
|
94
|
+
wrapper = Yielder.class_variable_get(:@@exception_wrapper)
|
95
|
+
@result = @fiber.resume(wrapper.new(StopIteration)) if @fiber.alive?
|
96
|
+
return @result
|
97
|
+
rescue StopIteration
|
98
|
+
return @result
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
include Sink
|
103
|
+
end
|
104
|
+
|
105
|
+
# A class implementing transformer coroutines
|
106
|
+
#
|
107
|
+
# A Transformer can be created by the following methods:
|
108
|
+
# * Object#trans_for
|
109
|
+
# * Transformer.new
|
110
|
+
#
|
111
|
+
# Transformers are pieces of code that accept input values and produce output
|
112
|
+
# values (without returning from their execution context like in a regular
|
113
|
+
# method call). They are used by connecting either their input to an enumerable
|
114
|
+
# or their output to a sink (or both, using Sink#<= or Enumerable#>=). An
|
115
|
+
# enumerable is any object implementing an iterator method each; a sink is
|
116
|
+
# any object implementing the << operator (which is assumed to store or
|
117
|
+
# output the supplied values in some form).
|
118
|
+
#
|
119
|
+
# The input of a transformer is connected to an enumerable +enum+ using
|
120
|
+
# trans <= enum, the result of which is a new Enumerator instance. The
|
121
|
+
# transformer is started upon iterating over this Enumerator. Whenever the
|
122
|
+
# transformer requests a new input value (see Object#trans_for and
|
123
|
+
# Transformer#new for how to do this), iteration over +enum+ is resumed. The
|
124
|
+
# output values of the transformer (again, see Object#trans_for and
|
125
|
+
# Transformer#new) are yielded by the enclosing Enumerator. When +enum+ is
|
126
|
+
# exhausted, StopIteration is raised at the point where the transformer last
|
127
|
+
# requested an input value. It is expected that the transformer will then
|
128
|
+
# terminate without requesting any more values (though it may execute e.g. some
|
129
|
+
# cleanup actions).
|
130
|
+
#
|
131
|
+
# The output of a transformer is connected to a sink using trans >= sink, the
|
132
|
+
# result of which is a new Consumer instance. The transformer starts
|
133
|
+
# executing right away; when it requests its first input value, the >=
|
134
|
+
# operation returns. Input values supplied using << to the enclosing
|
135
|
+
# Consumer are forwarded to the transformer by resuming execution at the
|
136
|
+
# point where it last requested an input value. Output values produced by the
|
137
|
+
# transformer are fed to sink#<<. After terminating, the result of the new
|
138
|
+
# Consumer is the value returned by sink.close (see Consumer#result and
|
139
|
+
# Consumer#close).
|
140
|
+
#
|
141
|
+
# Transformers can also be chained together by connecting the output of one the
|
142
|
+
# input the next using trans >= other_trans or trans <= other_trans. See
|
143
|
+
# Transformer#>= and Transformer#<= for details.
|
144
|
+
#
|
145
|
+
class Transformer
|
146
|
+
# :call-seq:
|
147
|
+
# Transformer.new { |yielder| ... } -> trans
|
148
|
+
#
|
149
|
+
# Creates a new Transformer coroutine defined by the given block.
|
150
|
+
#
|
151
|
+
# The block is called with a "yielder" object as parameter. +yielder+ can
|
152
|
+
# be used to retrieve a value from the consumption context by calling its
|
153
|
+
# await method (as in Consumer.new), and to yield a value by calling
|
154
|
+
# its yield method (as in Enumerator.new).
|
155
|
+
#
|
156
|
+
# running_sum = Transformer.new do |y|
|
157
|
+
# result = 0
|
158
|
+
# loop { result += y.await; y.yield result }
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
# (1..3) >= running_sum >= [] # => [1, 3, 6]
|
162
|
+
#
|
163
|
+
def initialize(&block)
|
164
|
+
@self = block
|
165
|
+
end
|
166
|
+
|
167
|
+
def inspect
|
168
|
+
"#<Transformer: 0x#{object_id.to_s(16)}>"
|
169
|
+
end
|
170
|
+
|
171
|
+
# :call-seq:
|
172
|
+
# transformer.to_trans -> transformer
|
173
|
+
#
|
174
|
+
# Returns self.
|
175
|
+
def to_trans
|
176
|
+
self
|
177
|
+
end
|
178
|
+
|
179
|
+
# :call-seq:
|
180
|
+
# trans <= other_trans -> new_trans
|
181
|
+
# trans <= enum -> new_enum
|
182
|
+
#
|
183
|
+
# In the first form, creates a new Transformer that has the input of
|
184
|
+
# +trans+ connected to the output of +other_trans+.
|
185
|
+
#
|
186
|
+
# In the second form, creates a new Enumerator by connecting +enum+ to
|
187
|
+
# the input of +trans+. See Transformer for details.
|
188
|
+
def <=(source)
|
189
|
+
if not source.respond_to? :each
|
190
|
+
return source.to_trans.transformer_chain self
|
191
|
+
end
|
192
|
+
|
193
|
+
source_enum = source.to_enum
|
194
|
+
enum = Enumerator.new do |y|
|
195
|
+
y.define_singleton_method :await do
|
196
|
+
source_enum.next
|
197
|
+
end
|
198
|
+
@self.call(y)
|
199
|
+
end
|
200
|
+
|
201
|
+
description = "#<Enumerator: #{inspect} <= #{source.inspect}>"
|
202
|
+
enum.define_singleton_method :inspect do
|
203
|
+
description
|
204
|
+
end
|
205
|
+
|
206
|
+
enum
|
207
|
+
end
|
208
|
+
|
209
|
+
# :call-seq:
|
210
|
+
# trans >= other_trans -> new_trans
|
211
|
+
# trans >= sink -> consum
|
212
|
+
#
|
213
|
+
# In the first form, creates a new Transformer that has the output of
|
214
|
+
# +trans+ connected to the input of +other_trans+.
|
215
|
+
#
|
216
|
+
# In the second form, creates a new Consumer by connecting the output of
|
217
|
+
# +trans+ to +sink+. See Transformer for details.
|
218
|
+
def >=(sink)
|
219
|
+
if not sink.respond_to? :<<
|
220
|
+
return transformer_chain sink.to_trans
|
221
|
+
end
|
222
|
+
|
223
|
+
consum = Consumer.new do |y|
|
224
|
+
y.define_singleton_method :yield do |args|
|
225
|
+
sink << args
|
226
|
+
end
|
227
|
+
y.alias_method :<<, :yield
|
228
|
+
begin
|
229
|
+
@self.call(y)
|
230
|
+
rescue StopIteration
|
231
|
+
end
|
232
|
+
sink.close
|
233
|
+
end
|
234
|
+
|
235
|
+
description = "#<Consumer: #{inspect} >= #{sink.inspect}>"
|
236
|
+
consum.define_singleton_method :inspect do
|
237
|
+
description
|
238
|
+
end
|
239
|
+
|
240
|
+
consum
|
241
|
+
end
|
242
|
+
|
243
|
+
protected
|
244
|
+
|
245
|
+
class FirstYielder < Consumer::Yielder
|
246
|
+
if Fiber.instance_methods.include? :raise
|
247
|
+
def await
|
248
|
+
Fiber.yield(:await)
|
249
|
+
end
|
250
|
+
def yield(*args)
|
251
|
+
Fiber.yield(:yield, *args)
|
252
|
+
end
|
253
|
+
alias_method :<<, :yield
|
254
|
+
else
|
255
|
+
def await
|
256
|
+
item = Fiber.yield(:await)
|
257
|
+
raise(*item.args) if item.instance_of? @@exception_wrapper
|
258
|
+
return item
|
259
|
+
end
|
260
|
+
def yield(*args)
|
261
|
+
item = Fiber.yield(:yield, *args)
|
262
|
+
raise(*item.args) if item.instance_of? @@exception_wrapper
|
263
|
+
end
|
264
|
+
alias_method :<<, :yield
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
class SecondYielder < Consumer::Yielder
|
269
|
+
def initialize(y, fiber)
|
270
|
+
@y, @fiber = y, fiber
|
271
|
+
end
|
272
|
+
|
273
|
+
if Fiber.instance_methods.include? :raise
|
274
|
+
def await
|
275
|
+
raise StopIteration unless @fiber.alive?
|
276
|
+
tag, result = @fiber.resume
|
277
|
+
while tag == :await do
|
278
|
+
x = nil
|
279
|
+
begin
|
280
|
+
x = @y.await
|
281
|
+
rescue Exception => e
|
282
|
+
tag, result = @fiber.raise(e)
|
283
|
+
end
|
284
|
+
tag, result = @fiber.resume(*x) unless x.nil?
|
285
|
+
raise StopIteration unless @fiber.alive?
|
286
|
+
end
|
287
|
+
result
|
288
|
+
end
|
289
|
+
else
|
290
|
+
def await
|
291
|
+
raise StopIteration unless @fiber.alive?
|
292
|
+
tag, result = @fiber.resume
|
293
|
+
while tag == :await do
|
294
|
+
begin
|
295
|
+
x = @y.await
|
296
|
+
rescue Exception => e
|
297
|
+
x = [@@exception_wrapper.new(e)]
|
298
|
+
end
|
299
|
+
tag, result = @fiber.resume(*x) if @fiber.alive?
|
300
|
+
raise StopIteration unless @fiber.alive?
|
301
|
+
end
|
302
|
+
result
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def yield(*args)
|
307
|
+
@y.yield(*args)
|
308
|
+
end
|
309
|
+
alias_method :<<, :yield
|
310
|
+
end
|
311
|
+
|
312
|
+
def transformer_chain(other)
|
313
|
+
first = @self
|
314
|
+
second = other.instance_variable_get(:@self)
|
315
|
+
|
316
|
+
trans = Transformer.new do |y|
|
317
|
+
fib = Fiber.new { first.call FirstYielder.new }
|
318
|
+
second.call(SecondYielder.new y, fib)
|
319
|
+
end
|
320
|
+
|
321
|
+
description = "#{inspect} >= #{other.inspect}"
|
322
|
+
trans.define_singleton_method :inspect do
|
323
|
+
description
|
324
|
+
end
|
325
|
+
|
326
|
+
trans
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# The <code>Sink</code> mixin provides classes that can accept streams of
|
2
|
+
# values with several utility methods. Classes using this mixin must at least
|
3
|
+
# provide an operator << which accepts a value and returns the sink instance
|
4
|
+
# (allowing chaining). Optionally, the class may provide a close method that
|
5
|
+
# signals end of input and (also optionally) retrives a result value.
|
6
|
+
module Sink
|
7
|
+
# May be overriden by classes using the Sink mixin. The default
|
8
|
+
# implementation just returns self.
|
9
|
+
def close
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
# :call-seq:
|
14
|
+
# sink <= enum -> obj
|
15
|
+
# sink <= trans -> new_consum
|
16
|
+
#
|
17
|
+
# In the first form, iterate over +enum+ and write each result to +sink+
|
18
|
+
# using <<; then return the result of sink.close.
|
19
|
+
#
|
20
|
+
# In the second form, create a new Consumer by connecting the output of
|
21
|
+
# +trans+ (which must be convertible to a Transformer using the
|
22
|
+
# to_trans method) to +sink+.
|
23
|
+
def <=(other)
|
24
|
+
if other.respond_to? :each
|
25
|
+
begin
|
26
|
+
other.each { |x| self << x }
|
27
|
+
rescue StopIteration
|
28
|
+
end
|
29
|
+
close
|
30
|
+
elsif other.respond_to? :to_trans
|
31
|
+
other >= self
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# :call-seq:
|
36
|
+
# sink.input_map{ |obj| block } -> new_sink
|
37
|
+
#
|
38
|
+
# Returns a new sink which supplies each of its input values to the given
|
39
|
+
# block and feeds each result of the block to +sink+.
|
40
|
+
def input_map(&block)
|
41
|
+
InputMapWrapper.new(self, &block)
|
42
|
+
end
|
43
|
+
|
44
|
+
# :call-seq:
|
45
|
+
# sink.input_select{ |obj| block } -> new_sink
|
46
|
+
#
|
47
|
+
# Returns a new sink which feeds those inputs for which +block+ returns a
|
48
|
+
# true value to +sink+ and discards all others.
|
49
|
+
def input_select
|
50
|
+
InputSelectWrapper.new(self, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
# :call-seq:
|
54
|
+
# sink.input_reject{ |obj| block } -> new_sink
|
55
|
+
#
|
56
|
+
# Returns a new sink which feeds those inputs for which +block+ returns a
|
57
|
+
# false value to +sink+ and discards all others.
|
58
|
+
def input_reject(&block)
|
59
|
+
InputRejectWrapper.new(self, &block)
|
60
|
+
end
|
61
|
+
|
62
|
+
# :call-seq:
|
63
|
+
# sink.input_reduce(initial=nil){ |memo, obj| block } -> new_sink
|
64
|
+
#
|
65
|
+
# Returns a new sink which reduces its input values to a single value, as
|
66
|
+
# in Enumerable#reduce. When +new_sink+ is closed, the reduced value is
|
67
|
+
# fed to +sink+ and +sink+ is closed as well.
|
68
|
+
def input_reduce(*args, &block)
|
69
|
+
InputReduceWrapper.new(self, *args, &block)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Collects multiple sinks into a multicast group. Every input value of the
|
73
|
+
# multicast group is supplied to each of its members. When the multicast
|
74
|
+
# group is closed, all members are closed as well.
|
75
|
+
class Multicast
|
76
|
+
def initialize(*receivers)
|
77
|
+
@receivers = receivers
|
78
|
+
end
|
79
|
+
def <<(obj)
|
80
|
+
@receivers.each { |r| r << obj }
|
81
|
+
self
|
82
|
+
end
|
83
|
+
def close
|
84
|
+
@receivers.each(&:close)
|
85
|
+
end
|
86
|
+
include Sink
|
87
|
+
end
|
88
|
+
|
89
|
+
######################################################################
|
90
|
+
private
|
91
|
+
######################################################################
|
92
|
+
|
93
|
+
class InputWrapper
|
94
|
+
def initialize(target, &block)
|
95
|
+
@target = target; @block = block
|
96
|
+
end
|
97
|
+
def close
|
98
|
+
@target.close
|
99
|
+
end
|
100
|
+
include Sink
|
101
|
+
end
|
102
|
+
|
103
|
+
class InputMapWrapper < InputWrapper
|
104
|
+
def <<(args)
|
105
|
+
@target << @block.call(args)
|
106
|
+
end
|
107
|
+
def inspect
|
108
|
+
"#<#{@target.inspect}#input_map>"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class InputSelectWrapper < InputWrapper
|
113
|
+
def <<(args)
|
114
|
+
@target << args if @block.call(args)
|
115
|
+
end
|
116
|
+
def inspect
|
117
|
+
"#<#{@target.inspect}#select>"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class InputRejectWrapper < InputWrapper
|
122
|
+
def <<(args)
|
123
|
+
@target << args unless @block.call(args)
|
124
|
+
end
|
125
|
+
def inspect
|
126
|
+
"#<#{@target.inspect}#reject>"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class InputReduceWrapper
|
131
|
+
def initialize(target, initial=nil, &block)
|
132
|
+
@target = target; @block = block
|
133
|
+
@memo = initial
|
134
|
+
end
|
135
|
+
def <<(arg)
|
136
|
+
if @memo.nil?
|
137
|
+
@memo = arg
|
138
|
+
else
|
139
|
+
@memo = @block.call(@memo, arg)
|
140
|
+
end
|
141
|
+
self
|
142
|
+
end
|
143
|
+
def close
|
144
|
+
@target << @memo
|
145
|
+
@target.close
|
146
|
+
end
|
147
|
+
include Sink
|
148
|
+
end
|
149
|
+
end
|
data/lib/coroutines.rb
ADDED
@@ -0,0 +1,334 @@
|
|
1
|
+
require 'coroutines/base'
|
2
|
+
|
3
|
+
#--
|
4
|
+
# Most of the Sink methods mirror Enumerable, except for <=
|
5
|
+
# for symmetry, also add a >= method to Enumerable
|
6
|
+
#++
|
7
|
+
module Enumerable
|
8
|
+
# :call-seq:
|
9
|
+
# enum >= sink -> obj
|
10
|
+
# enum >= trans -> new_enum
|
11
|
+
#
|
12
|
+
# In the first form, iterate over +enum+ and write each result to +sink+
|
13
|
+
# using <<; then return the result of sink.close.
|
14
|
+
#
|
15
|
+
# In the second form, create a new Enumerator by connecting +enum+ to the
|
16
|
+
# input of +trans+ (which must be convertible to a Transformer using the
|
17
|
+
# to_trans method).
|
18
|
+
def >=(other)
|
19
|
+
if other.respond_to? :<<
|
20
|
+
begin
|
21
|
+
each { |x| other << x }
|
22
|
+
rescue StopIteration
|
23
|
+
end
|
24
|
+
other.close
|
25
|
+
elsif other.respond_to? :to_trans
|
26
|
+
other <= self
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# convenience alias for Sink::Multicast
|
32
|
+
# (this is not defined when requiring just coroutines/base)
|
33
|
+
Multicast = Sink::Multicast
|
34
|
+
|
35
|
+
class IO
|
36
|
+
include Sink
|
37
|
+
end
|
38
|
+
|
39
|
+
class Array
|
40
|
+
include Sink
|
41
|
+
end
|
42
|
+
|
43
|
+
class String
|
44
|
+
include Sink
|
45
|
+
end
|
46
|
+
|
47
|
+
class Hash
|
48
|
+
# :call-seq:
|
49
|
+
# hash << [key, value] -> hash
|
50
|
+
#
|
51
|
+
# Equivalent to hash[key] = value, but allows chaining:
|
52
|
+
#
|
53
|
+
# {} << [1, "one"] << [2, "two"] << [3, "three"] #=> {1=>"one", 2=>"two", 3=>"three"}
|
54
|
+
def <<(args)
|
55
|
+
self[args[0]] = args[1]
|
56
|
+
self
|
57
|
+
end
|
58
|
+
include Sink
|
59
|
+
end
|
60
|
+
|
61
|
+
class Method
|
62
|
+
# :call-seq:
|
63
|
+
# method << [arg1, arg2, ...] -> method
|
64
|
+
#
|
65
|
+
# Equivalent to method.call(arg1, arg2, ...), but allows chaining:
|
66
|
+
#
|
67
|
+
# method(:puts) << 1 << 2 << 3 # print each number to stdout
|
68
|
+
def <<(args)
|
69
|
+
call(*args)
|
70
|
+
self
|
71
|
+
end
|
72
|
+
alias_method :close, :receiver
|
73
|
+
include Sink
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
class CoroutineError < StandardError
|
78
|
+
end
|
79
|
+
|
80
|
+
# :call-seq:
|
81
|
+
# await -> obj
|
82
|
+
#
|
83
|
+
# Evaluates to the next input value from the associated consumption context. In
|
84
|
+
# order to create a consumption context, you have to use Object#consum_for or
|
85
|
+
# Object#trans_for on the method calling await.
|
86
|
+
#
|
87
|
+
# The behavior of await is undefined if the method using it is called without
|
88
|
+
# being wrapped by consum_for or trans_for. It should be an error to do so, as
|
89
|
+
# it is an error to use yield without an associated block; however, since await
|
90
|
+
# is not a language primitive (like yield), it is not always possible to
|
91
|
+
# enforce this. This is likely to change in a future version if a better
|
92
|
+
# solution can be found.
|
93
|
+
def await
|
94
|
+
yielder = Fiber.current.instance_variable_get(:@yielder)
|
95
|
+
raise CoroutineError, "you can't call a consumer" if yielder.nil?
|
96
|
+
yielder.await
|
97
|
+
end
|
98
|
+
|
99
|
+
class Object
|
100
|
+
# :call-seq:
|
101
|
+
# consum_for(method, *args) -> consumer
|
102
|
+
#
|
103
|
+
# Creates a new Consumer coroutine from the given method. This is analogous
|
104
|
+
# to using Kernel#enum_for to create an Enumerator instance. The method is
|
105
|
+
# called immediately (with the given +args+). It executes until the first
|
106
|
+
# call to #await, at which point consum_for returns the Consumer
|
107
|
+
# instance. Calling consumer << obj resumes the consumer at the point where
|
108
|
+
# it last executed #await, which evaluates to +obj+.
|
109
|
+
#
|
110
|
+
# Calling consumer.close raises StopIteration at the point where the
|
111
|
+
# consumer last executed #await; it is expected that it will
|
112
|
+
# terminate without executing #await again. Consumer#close evaluates
|
113
|
+
# to the return
|
114
|
+
# value of the method.
|
115
|
+
#
|
116
|
+
# Example:
|
117
|
+
#
|
118
|
+
# def counter(start)
|
119
|
+
# result = start
|
120
|
+
# loop { result += await }
|
121
|
+
# "Final value: #{result}"
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# co = consum_for :counter, 10 #=> #<Consumer: main:counter (running)>
|
125
|
+
# co << 10 << 1000 << 10000
|
126
|
+
# co.close #=> "Final value: 11020"
|
127
|
+
#
|
128
|
+
def consum_for(meth, *args)
|
129
|
+
cons = Consumer.new do |y|
|
130
|
+
Fiber.current.instance_variable_set(:@yielder, y)
|
131
|
+
send(meth, *args)
|
132
|
+
end
|
133
|
+
description = "#{inspect}:#{meth}"
|
134
|
+
cons.define_singleton_method :inspect do
|
135
|
+
state = if @fiber.alive? then "running" else @result.inspect end
|
136
|
+
"#<Consumer: #{description} (#{state})>"
|
137
|
+
end
|
138
|
+
cons
|
139
|
+
end
|
140
|
+
|
141
|
+
# :call-seq:
|
142
|
+
# trans_for(method, *args) -> transformer
|
143
|
+
#
|
144
|
+
# Creates a new Transformer instance that wraps the given method. This is
|
145
|
+
# analogous to using Kernel#enum_for to create an Enumerator instance, or
|
146
|
+
# using Object#consum_for to create a Consumer instance. The method is not
|
147
|
+
# executed immediately. The resulting Transformer can be connected to an
|
148
|
+
# enumerable (using transformer <= enum) or to a sink (using
|
149
|
+
# transformer >= sink). The point at which the transformer method gets
|
150
|
+
# started depends on how it is connected; in any case however, the method
|
151
|
+
# will be called with the +args+ given to trans_for. See Transformer
|
152
|
+
# for details.
|
153
|
+
#
|
154
|
+
# Within the transformer method, #await can be used to read the next
|
155
|
+
# input value (as in a Consumer, compare Object#consum_for), and yield can
|
156
|
+
# be used to produce an output value (i.e., when it is called, the method
|
157
|
+
# is given a block which accepts its output).
|
158
|
+
#
|
159
|
+
# Example:
|
160
|
+
#
|
161
|
+
# def running_sum(start)
|
162
|
+
# result = start
|
163
|
+
# loop { result += await; yield result }
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# tr = trans_for :running_sum, 3 #=> #<Transformer: main:running_sum>
|
167
|
+
# sums = (1..10) >= tr #=> #<Enumerator: #<Transformer: main:running_sum> <= 1..10>
|
168
|
+
# sums.to_a #=> [4, 6, 9, 13, 18, 24, 31, 39, 48, 58]
|
169
|
+
#
|
170
|
+
def trans_for(meth, *args)
|
171
|
+
trans = Transformer.new do |y|
|
172
|
+
Fiber.current.instance_variable_set(:@yielder, y)
|
173
|
+
send(meth, *args, &y.method(:yield))
|
174
|
+
end
|
175
|
+
description ="#{inspect}:#{meth}"
|
176
|
+
trans.define_singleton_method :inspect do
|
177
|
+
"#<Transformer: #{description}>"
|
178
|
+
end
|
179
|
+
trans
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class Symbol
|
184
|
+
# :call-seq:
|
185
|
+
# sym.to_trans -> transformer
|
186
|
+
#
|
187
|
+
# Allows implicit conversion of Symbol to Transformer. The transformer
|
188
|
+
# accepts any objects as input, calls the method given by +sym+ on each and
|
189
|
+
# outputs all non-nil results of the method. See Proc#to_trans for details.
|
190
|
+
#
|
191
|
+
# example:
|
192
|
+
#
|
193
|
+
# collector = [] <= :to_s
|
194
|
+
# collector << 1 << 4 << 9
|
195
|
+
# collector.close # => ["1", "4", "9"]
|
196
|
+
#
|
197
|
+
def to_trans
|
198
|
+
Transformer.new do |y|
|
199
|
+
loop do
|
200
|
+
value = y.await.send self
|
201
|
+
y.yield value unless value.nil?
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# :call-seq:
|
207
|
+
# sym <= trans -> new_trans
|
208
|
+
# sym <= enum -> new_enum
|
209
|
+
#
|
210
|
+
# Equivalent to sym.to_trans <= trans/enum, except that it uses a more
|
211
|
+
# efficient implementation.
|
212
|
+
def <=(source)
|
213
|
+
to_proc <= source
|
214
|
+
end
|
215
|
+
|
216
|
+
# :call-seq:
|
217
|
+
# sym >= trans -> new_trans
|
218
|
+
# sym >= sink -> new_consumer
|
219
|
+
#
|
220
|
+
# Equivalent to sym.to_trans >= trans/sink, except that it uses a more
|
221
|
+
# efficient implementation.
|
222
|
+
def >=(sink)
|
223
|
+
to_proc >= sink
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Define a poor man's Enumerator::Lazy for Ruby < 2.0
|
228
|
+
if defined? Enumerator::Lazy
|
229
|
+
LazyEnumerator = Enumerator::Lazy
|
230
|
+
else
|
231
|
+
class LazyEnumerator
|
232
|
+
def initialize(obj, &block)
|
233
|
+
@obj = obj; @block = block
|
234
|
+
end
|
235
|
+
|
236
|
+
class Yielder
|
237
|
+
def initialize(iter_block)
|
238
|
+
@iter_block = iter_block
|
239
|
+
end
|
240
|
+
def yield(*values)
|
241
|
+
@iter_block.call(*values)
|
242
|
+
end
|
243
|
+
alias_method :<<, :yield
|
244
|
+
end
|
245
|
+
|
246
|
+
def each(&iter_block)
|
247
|
+
yielder = Yielder.new(iter_block)
|
248
|
+
@obj.each do |*args|
|
249
|
+
@block.call(yielder, *args)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
include Enumerable
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
class Proc
|
257
|
+
# :call-seq:
|
258
|
+
# proc.to_trans -> transformer
|
259
|
+
#
|
260
|
+
# Allows implicit conversion of Proc to Transformer. The transformer is a
|
261
|
+
# combination of map and filter over its input values: For each input
|
262
|
+
# value, +proc+ is called with the input value as parameter. Every non-nil
|
263
|
+
# value returned by +proc+ is yielded as an output value.
|
264
|
+
#
|
265
|
+
# This is similar to Enumerable#map followed by Array#compact, but without
|
266
|
+
# constructing the intermediate Array (so it's even more similar to
|
267
|
+
# something like enum.lazy.map(&proc).reject(&:nil?), using the lazy
|
268
|
+
# enumerator introduced in Ruby 2.0).
|
269
|
+
#
|
270
|
+
# Example:
|
271
|
+
#
|
272
|
+
# (1..10) >= proc{|x| x.to_s + ", " if x.even? } >= ""
|
273
|
+
# # => "2, 4, 6, 8, 10, "
|
274
|
+
#
|
275
|
+
def to_trans
|
276
|
+
Transformer.new do |y|
|
277
|
+
loop do
|
278
|
+
value = self.call(y.await)
|
279
|
+
y.yield value unless value.nil?
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# :call-seq:
|
285
|
+
# proc <= trans -> new_trans
|
286
|
+
# proc <= enum -> new_enum
|
287
|
+
#
|
288
|
+
# Equivalent to proc.to_trans <= trans/enum, except that it uses a more
|
289
|
+
# efficient implementation.
|
290
|
+
def <=(source)
|
291
|
+
if source.respond_to? :each
|
292
|
+
LazyEnumerator.new(source) do |y, *args|
|
293
|
+
value = call(*args)
|
294
|
+
y << value unless value.nil?
|
295
|
+
end
|
296
|
+
elsif source.respond_to? :to_proc
|
297
|
+
sp = source.to_proc
|
298
|
+
proc do |*args|
|
299
|
+
value = sp.call(*args)
|
300
|
+
if value.nil? then nil else self.call value end
|
301
|
+
end
|
302
|
+
elsif source.respond_to? :to_trans
|
303
|
+
# FIXME: this could be implemented more efficiently; proc doesn't
|
304
|
+
# need to run in a separate Fiber
|
305
|
+
self.to_trans <= source.to_trans
|
306
|
+
else
|
307
|
+
raise ArgumentError, "#{source.inspect} is neither an enumerable nor a transformer"
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# :call-seq:
|
312
|
+
# proc >= trans -> new_trans
|
313
|
+
# proc >= sink -> new_consumer
|
314
|
+
#
|
315
|
+
# Equivalent to proc.to_trans >= trans/sink, except that it uses a more
|
316
|
+
# efficient implementation.
|
317
|
+
def >=(sink)
|
318
|
+
if sink.respond_to? :input_map
|
319
|
+
sink.input_reject(&:nil?).input_map(&self)
|
320
|
+
elsif sink.respond_to? :to_proc
|
321
|
+
sp = sink.to_proc
|
322
|
+
proc do |*args|
|
323
|
+
value = self.call(*args)
|
324
|
+
if value.nil? then nil else sp.call value end
|
325
|
+
end
|
326
|
+
elsif sink.respond_to? :to_trans
|
327
|
+
# FIXME: this could be implemented more efficiently; proc doesn't
|
328
|
+
# need to run in a separate Fiber
|
329
|
+
self.to_trans >= sink.to_trans
|
330
|
+
else
|
331
|
+
raise ArgumentError, "#{sink.inspect} is neither a sink nor a transformer"
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'coroutines'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
class TestCoroutines < Test::Unit::TestCase
|
5
|
+
def test_sink
|
6
|
+
assert_equal("abcdef", "abc" <= ("d".."f"))
|
7
|
+
assert_equal([1,2,3,4], [1] <= [2,3,4])
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_source
|
11
|
+
assert_equal("abcdef", ("d".."f") >= "abc")
|
12
|
+
assert_equal([1,2,3,4], [2,3,4] >= [1])
|
13
|
+
end
|
14
|
+
|
15
|
+
def test_consumer
|
16
|
+
c = Consumer.new { |y| [ y.await, y.await ] }
|
17
|
+
c << :first << :second
|
18
|
+
assert_equal([:first, :second], c.close)
|
19
|
+
end
|
20
|
+
|
21
|
+
def counter(start)
|
22
|
+
result = start
|
23
|
+
loop { result += await }
|
24
|
+
"Final value: #{result}"
|
25
|
+
end
|
26
|
+
def test_consumer_method
|
27
|
+
c = consum_for :counter, 10
|
28
|
+
c << 10 << 1000 << 10000
|
29
|
+
assert_equal("Final value: 11020", c.close)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_transformer
|
33
|
+
rs = Transformer.new do |y|
|
34
|
+
result = 0
|
35
|
+
loop { result += y.await; y.yield result }
|
36
|
+
end
|
37
|
+
assert_equal([1, 3, 6], (1..3) >= rs >= [])
|
38
|
+
|
39
|
+
rs = Transformer.new do |y|
|
40
|
+
result = 0
|
41
|
+
loop { result += y.await; y.yield result }
|
42
|
+
end
|
43
|
+
assert_equal([1, 3, 6], [] <= rs <= (1..3))
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_transformer_chaining
|
47
|
+
t1 = Transformer.new{|y| y.yield (y.await + "a")}
|
48
|
+
t2 = Transformer.new{|y| y.yield (y.await + "b")}
|
49
|
+
t = t1 >= t2
|
50
|
+
result = %w{x y z} >= t >= []
|
51
|
+
assert_equal(["xab"], result)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_associativity
|
55
|
+
s = (1..3)
|
56
|
+
def t1
|
57
|
+
Transformer.new{|y| loop { y.yield y.await.to_s } }
|
58
|
+
end
|
59
|
+
def t2
|
60
|
+
Transformer.new{|y| loop { y.yield (y.await + ",") } }
|
61
|
+
end
|
62
|
+
|
63
|
+
assert_equal("1,2,3,", s >= t1 >= t2 >= "")
|
64
|
+
assert_equal("1,2,3,", s >= t1 >= (t2 >= ""))
|
65
|
+
assert_equal("1,2,3,", s >= (t1 >= t2 >= ""))
|
66
|
+
assert_equal("1,2,3,", s >= (t1 >= t2) >= "")
|
67
|
+
assert_equal("1,2,3,", s >= ((t1 >= t2) >= ""))
|
68
|
+
|
69
|
+
assert_equal("1,2,3,", "" <= t2 <= t1 <= s)
|
70
|
+
assert_equal("1,2,3,", "" <= t2 <= (t1 <= s))
|
71
|
+
assert_equal("1,2,3,", "" <= (t2 <= t1 <= s))
|
72
|
+
assert_equal("1,2,3,", "" <= (t2 <= t1) <= s)
|
73
|
+
assert_equal("1,2,3,", "" <= ((t2 <= t1) <= s))
|
74
|
+
end
|
75
|
+
|
76
|
+
def running_sum(start)
|
77
|
+
result = start
|
78
|
+
loop { result += await; yield result }
|
79
|
+
end
|
80
|
+
def test_transformer_method
|
81
|
+
assert_equal([4, 6, 9], (1..3) >= trans_for(:running_sum, 3) >= [])
|
82
|
+
assert_equal([4, 6, 9], (1..3) >= (trans_for(:running_sum, 3) >= []))
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_stop_iteration
|
86
|
+
consume_three = Consumer.new do |y|
|
87
|
+
[y.await, y.await, y.await]
|
88
|
+
end
|
89
|
+
assert_equal([1,2,3], (1..Float::INFINITY) >= consume_three)
|
90
|
+
|
91
|
+
limit_three = Transformer.new do |y|
|
92
|
+
3.times { y.yield y.await }
|
93
|
+
end
|
94
|
+
assert_equal([1,2,3], (1..Float::INFINITY) >= limit_three >= [])
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_transformer_procs
|
98
|
+
s = (1..3)
|
99
|
+
t1_proc = proc{|x| x.to_s }
|
100
|
+
t2_proc = proc{|x| x + "," }
|
101
|
+
def t1
|
102
|
+
Transformer.new{|y| loop { y.yield y.await.to_s } }
|
103
|
+
end
|
104
|
+
def t2
|
105
|
+
Transformer.new{|y| loop { y.yield (y.await + ",") } }
|
106
|
+
end
|
107
|
+
|
108
|
+
assert_equal("1,2,3,", s >= t1_proc >= t2_proc >= "")
|
109
|
+
assert_equal("1,2,3,", s >= t1_proc >= (t2_proc >= ""))
|
110
|
+
assert_equal("1,2,3,", s >= (t1_proc >= t2_proc >= ""))
|
111
|
+
assert_equal("1,2,3,", s >= (t1_proc >= t2_proc) >= "")
|
112
|
+
assert_equal("1,2,3,", s >= ((t1_proc >= t2_proc) >= ""))
|
113
|
+
|
114
|
+
assert_equal("1,2,3,", "" <= t2_proc <= t1_proc <= s)
|
115
|
+
assert_equal("1,2,3,", "" <= t2_proc <= (t1_proc <= s))
|
116
|
+
assert_equal("1,2,3,", "" <= (t2_proc <= t1_proc <= s))
|
117
|
+
assert_equal("1,2,3,", "" <= (t2_proc <= t1_proc) <= s)
|
118
|
+
assert_equal("1,2,3,", "" <= ((t2_proc <= t1_proc) <= s))
|
119
|
+
|
120
|
+
assert_equal("1,2,3,", s >= t1_proc >= t2 >= "")
|
121
|
+
assert_equal("1,2,3,", s >= t1_proc >= (t2 >= ""))
|
122
|
+
assert_equal("1,2,3,", s >= (t1_proc >= t2 >= ""))
|
123
|
+
assert_equal("1,2,3,", s >= (t1_proc >= t2) >= "")
|
124
|
+
assert_equal("1,2,3,", s >= ((t1_proc >= t2) >= ""))
|
125
|
+
|
126
|
+
assert_equal("1,2,3,", "" <= t2 <= t1_proc <= s)
|
127
|
+
assert_equal("1,2,3,", "" <= t2 <= (t1_proc <= s))
|
128
|
+
assert_equal("1,2,3,", "" <= (t2 <= t1_proc <= s))
|
129
|
+
assert_equal("1,2,3,", "" <= (t2 <= t1_proc) <= s)
|
130
|
+
assert_equal("1,2,3,", "" <= ((t2 <= t1_proc) <= s))
|
131
|
+
|
132
|
+
assert_equal("1,2,3,", s >= t1 >= t2_proc >= "")
|
133
|
+
assert_equal("1,2,3,", s >= t1 >= (t2_proc >= ""))
|
134
|
+
assert_equal("1,2,3,", s >= (t1 >= t2_proc >= ""))
|
135
|
+
assert_equal("1,2,3,", s >= (t1 >= t2_proc) >= "")
|
136
|
+
assert_equal("1,2,3,", s >= ((t1 >= t2_proc) >= ""))
|
137
|
+
|
138
|
+
assert_equal("1,2,3,", "" <= t2_proc <= t1 <= s)
|
139
|
+
assert_equal("1,2,3,", "" <= t2_proc <= (t1 <= s))
|
140
|
+
assert_equal("1,2,3,", "" <= (t2_proc <= t1 <= s))
|
141
|
+
assert_equal("1,2,3,", "" <= (t2_proc <= t1) <= s)
|
142
|
+
assert_equal("1,2,3,", "" <= ((t2_proc <= t1) <= s))
|
143
|
+
end
|
144
|
+
|
145
|
+
def test_transformer_proc_filter
|
146
|
+
assert_equal("2468", (1..9) >= proc{|x| x.to_s if x.even? } >= "")
|
147
|
+
assert_equal("2468", (1..9) >= proc{|x| x if x.even? } >= proc{|x| x.to_s} >= "")
|
148
|
+
assert_equal("56789", (5..15) >= proc{|x| x.to_s } >= proc{|s| s if s.length == 1 } >= "")
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_multiple_args
|
152
|
+
assert_equal([[1,2],[3,4]], [[1,2],[3,4]] >= proc{|a,b| [a,b]} >= [])
|
153
|
+
end
|
154
|
+
end
|
metadata
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: coroutines
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Knut Franke
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-11 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: |
|
14
|
+
A library for creating and composing producer/transformer/consumer coroutines.
|
15
|
+
Producers are already provided by Ruby's built-in Enumerator class; this
|
16
|
+
library provides Transformer and Consumer classes that work analogously. In
|
17
|
+
particular, they are also based on Fiber and not on threads (as in some other
|
18
|
+
producer/consumer libraries). Also provides a module Sink, which is analogous
|
19
|
+
to Enumerable, and Enumerable/Transformer/Sink composition using overloaded
|
20
|
+
<= and >= operators.
|
21
|
+
email: knut.franke@gmx.de
|
22
|
+
executables: []
|
23
|
+
extensions: []
|
24
|
+
extra_rdoc_files:
|
25
|
+
- README.rdoc
|
26
|
+
files:
|
27
|
+
- README.rdoc
|
28
|
+
- lib/coroutines.rb
|
29
|
+
- lib/coroutines/base.rb
|
30
|
+
- lib/coroutines/sink.rb
|
31
|
+
- tests/test_coroutines.rb
|
32
|
+
homepage: https://github.com/nome/coroutines
|
33
|
+
licenses:
|
34
|
+
- Ruby
|
35
|
+
- BSD-2-Clause
|
36
|
+
metadata: {}
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 2.4.2
|
54
|
+
signing_key:
|
55
|
+
specification_version: 4
|
56
|
+
summary: Library for producer/transformer/consumer coroutines
|
57
|
+
test_files:
|
58
|
+
- tests/test_coroutines.rb
|
59
|
+
has_rdoc: false
|