functional-ruby 0.5.0 → 0.6.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +154 -562
  3. data/lib/functional/agent.rb +130 -0
  4. data/lib/functional/all.rb +9 -1
  5. data/lib/functional/behavior.rb +72 -39
  6. data/lib/functional/cached_thread_pool.rb +122 -0
  7. data/lib/functional/concurrency.rb +32 -24
  8. data/lib/functional/core.rb +2 -62
  9. data/lib/functional/event.rb +53 -0
  10. data/lib/functional/event_machine_defer_proxy.rb +23 -0
  11. data/lib/functional/fixed_thread_pool.rb +89 -0
  12. data/lib/functional/future.rb +42 -0
  13. data/lib/functional/global_thread_pool.rb +3 -0
  14. data/lib/functional/obligation.rb +121 -0
  15. data/lib/functional/promise.rb +194 -0
  16. data/lib/functional/thread_pool.rb +61 -0
  17. data/lib/functional/utilities.rb +114 -0
  18. data/lib/functional/version.rb +1 -1
  19. data/lib/functional.rb +1 -0
  20. data/lib/functional_ruby.rb +1 -0
  21. data/md/behavior.md +147 -0
  22. data/md/concurrency.md +465 -0
  23. data/md/future.md +32 -0
  24. data/md/obligation.md +32 -0
  25. data/md/pattern_matching.md +512 -0
  26. data/md/promise.md +220 -0
  27. data/md/utilities.md +53 -0
  28. data/spec/functional/agent_spec.rb +405 -0
  29. data/spec/functional/behavior_spec.rb +12 -33
  30. data/spec/functional/cached_thread_pool_spec.rb +112 -0
  31. data/spec/functional/concurrency_spec.rb +55 -0
  32. data/spec/functional/event_machine_defer_proxy_spec.rb +246 -0
  33. data/spec/functional/event_spec.rb +114 -0
  34. data/spec/functional/fixed_thread_pool_spec.rb +84 -0
  35. data/spec/functional/future_spec.rb +115 -0
  36. data/spec/functional/obligation_shared.rb +121 -0
  37. data/spec/functional/pattern_matching_spec.rb +10 -8
  38. data/spec/functional/promise_spec.rb +310 -0
  39. data/spec/functional/thread_pool_shared.rb +209 -0
  40. data/spec/functional/utilities_spec.rb +149 -0
  41. data/spec/spec_helper.rb +2 -0
  42. metadata +55 -5
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+ require_relative 'obligation_shared'
3
+
4
+ module Functional
5
+
6
+ describe Future do
7
+
8
+ let!(:fulfilled_value) { 10 }
9
+ let!(:rejected_reason) { StandardError.new('mojo jojo') }
10
+
11
+ let(:pending_subject) do
12
+ Future.new{ sleep(2) }
13
+ end
14
+
15
+ let(:fulfilled_subject) do
16
+ Future.new{ fulfilled_value }.tap(){ sleep(0.1) }
17
+ end
18
+
19
+ let(:rejected_subject) do
20
+ Future.new{ raise rejected_reason }.tap(){ sleep(0.1) }
21
+ end
22
+
23
+ before(:each) do
24
+ $GLOBAL_THREAD_POOL = CachedThreadPool.new
25
+ end
26
+
27
+ it_should_behave_like Obligation
28
+
29
+ context 'behavior' do
30
+
31
+ it 'implements :future behavior' do
32
+ lambda {
33
+ Future.new{ nil }
34
+ }.should_not raise_error(BehaviorError)
35
+
36
+ Future.new{ nil }.behaves_as?(:future).should be_true
37
+ end
38
+ end
39
+
40
+ context '#initialize' do
41
+
42
+ it 'spawns a new thread when a block is given' do
43
+ $GLOBAL_THREAD_POOL.should_receive(:post).once.with(any_args())
44
+ Future.new{ nil }
45
+ end
46
+
47
+ it 'does not spawns a new thread when no block given' do
48
+ Thread.should_not_receive(:new).with(any_args())
49
+ Future.new
50
+ end
51
+
52
+ it 'immediately sets the state to :fulfilled when no block given' do
53
+ Future.new.should be_fulfilled
54
+ end
55
+
56
+ it 'immediately sets the value to nil when no block given' do
57
+ Future.new.value.should be_nil
58
+ end
59
+ end
60
+
61
+ context 'fulfillment' do
62
+
63
+ it 'passes all arguments to handler' do
64
+ @a = @b = @c = nil
65
+ f = Future.new(1, 2, 3) do |a, b, c|
66
+ @a, @b, @c = a, b, c
67
+ end
68
+ sleep(0.1)
69
+ [@a, @b, @c].should eq [1, 2, 3]
70
+ end
71
+
72
+ it 'sets the value to the result of the handler' do
73
+ f = Future.new(10){|a| a * 2 }
74
+ sleep(0.1)
75
+ f.value.should eq 20
76
+ end
77
+
78
+ it 'sets the state to :fulfilled when the block completes' do
79
+ f = Future.new(10){|a| a * 2 }
80
+ sleep(0.1)
81
+ f.should be_fulfilled
82
+ end
83
+
84
+ it 'sets the value to nil when the handler raises an exception' do
85
+ f = Future.new{ raise StandardError }
86
+ sleep(0.1)
87
+ f.value.should be_nil
88
+ end
89
+
90
+ it 'sets the state to :rejected when the handler raises an exception' do
91
+ f = Future.new{ raise StandardError }
92
+ sleep(0.1)
93
+ f.should be_rejected
94
+ end
95
+
96
+ context 'aliases' do
97
+
98
+ it 'aliases #realized? for #fulfilled?' do
99
+ fulfilled_subject.should be_realized
100
+ end
101
+
102
+ it 'aliases #deref for #value' do
103
+ fulfilled_subject.deref.should eq fulfilled_value
104
+ end
105
+
106
+ it 'aliases Kernel#future for Future.new' do
107
+ future().should be_a(Future)
108
+ future(){ nil }.should be_a(Future)
109
+ future(1, 2, 3).should be_a(Future)
110
+ future(1, 2, 3){ nil }.should be_a(Future)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,121 @@
1
+ require 'spec_helper'
2
+
3
+ module Functional
4
+
5
+ share_examples_for Obligation do
6
+
7
+ context '#state' do
8
+
9
+ it 'is :pending when first created' do
10
+ f = pending_subject
11
+ f.state.should == :pending
12
+ f.should be_pending
13
+ end
14
+
15
+ it 'is :fulfilled when the handler completes' do
16
+ f = fulfilled_subject
17
+ f.state.should == :fulfilled
18
+ f.should be_fulfilled
19
+ end
20
+
21
+ it 'is :rejected when the handler raises an exception' do
22
+ f = rejected_subject
23
+ f.state.should == :rejected
24
+ f.should be_rejected
25
+ end
26
+ end
27
+
28
+ context '#value' do
29
+
30
+ it 'blocks the caller when :pending and timeout is nil' do
31
+ f = pending_subject
32
+ sleep(0.1)
33
+ f.value.should be_true
34
+ f.should be_fulfilled
35
+ end
36
+
37
+ it 'returns nil when reaching the optional timeout value' do
38
+ f = pending_subject
39
+ sleep(0.1)
40
+ f.value(0.1).should be_nil
41
+ f.should be_pending
42
+ end
43
+
44
+ it 'returns immediately when timeout is zero' do
45
+ Timeout.should_not_receive(:timeout).with(any_args())
46
+ f = pending_subject
47
+ sleep(0.1)
48
+ f.value(0).should be_nil
49
+ f.should be_pending
50
+ end
51
+
52
+ it 'is nil when :pending' do
53
+ expected = pending_subject.value
54
+ expected.should be_nil
55
+ end
56
+
57
+ it 'is nil when :rejected' do
58
+ expected = rejected_subject.value
59
+ expected.should be_nil
60
+ end
61
+
62
+ it 'is set to the return value of the block when :fulfilled' do
63
+ expected = fulfilled_subject.value
64
+ expected.should eq fulfilled_value
65
+ end
66
+ end
67
+
68
+ context '#reason' do
69
+
70
+ it 'is nil when :pending' do
71
+ pending_subject.reason.should be_nil
72
+ end
73
+
74
+ it 'is nil when :fulfilled' do
75
+ fulfilled_subject.reason.should be_nil
76
+ end
77
+
78
+ it 'is set to error object of the exception when :rejected' do
79
+ rejected_subject.reason.should be_a(Exception)
80
+ rejected_subject.reason.to_s.should =~ /#{rejected_reason}/
81
+ end
82
+ end
83
+
84
+ context 'Kernel aliases' do
85
+
86
+ it 'aliases Kernel#deref for #deref' do
87
+ deref(fulfilled_subject).should eq fulfilled_value
88
+ deref(fulfilled_subject, 0).should eq fulfilled_value
89
+ end
90
+
91
+ it 'aliases Kernel#pending? for #pending?' do
92
+ #NOTE: was structured like others but was incorrectly failing
93
+ # on fulfilled_subject
94
+ fulfilled_subject.should_receive(:pending?).once
95
+ pending?(fulfilled_subject)
96
+ pending_subject.should_receive(:pending?).once
97
+ pending?(pending_subject)
98
+ rejected_subject.should_receive(:pending?).once
99
+ pending?(rejected_subject)
100
+ end
101
+
102
+ it 'aliases Kernel#fulfilled? for #fulfilled?' do
103
+ fulfilled?(fulfilled_subject).should be_true
104
+ fulfilled?(pending_subject).should be_false
105
+ fulfilled?(rejected_subject).should be_false
106
+ end
107
+
108
+ it 'aliases Kernel#realized? for #realized?' do
109
+ realized?(fulfilled_subject).should be_true
110
+ realized?(pending_subject).should be_false
111
+ realized?(rejected_subject).should be_false
112
+ end
113
+
114
+ it 'aliases Kernel#rejected? for #rejected?' do
115
+ rejected?(rejected_subject).should be_true
116
+ rejected?(fulfilled_subject).should be_false
117
+ rejected?(pending_subject).should be_false
118
+ end
119
+ end
120
+ end
121
+ end
@@ -44,14 +44,16 @@ describe PatternMatching do
44
44
 
45
45
  it 'can pattern match the constructor' do
46
46
 
47
- subject.defn(:initialize, PatternMatching::UNBOUND, PatternMatching::UNBOUND, PatternMatching::UNBOUND) { 'three args' }
48
- subject.defn(:initialize, PatternMatching::UNBOUND, PatternMatching::UNBOUND) { 'two args' }
49
- subject.defn(:initialize, PatternMatching::UNBOUND) { 'one arg' }
50
-
51
- lambda { subject.new(1) }.should_not raise_error
52
- lambda { subject.new(1, 2) }.should_not raise_error
53
- lambda { subject.new(1, 2, 3) }.should_not raise_error
54
- lambda { subject.new(1, 2, 3, 4) }.should raise_error
47
+ unless RUBY_VERSION = '1.9.2'
48
+ subject.defn(:initialize, PatternMatching::UNBOUND, PatternMatching::UNBOUND, PatternMatching::UNBOUND) { 'three args' }
49
+ subject.defn(:initialize, PatternMatching::UNBOUND, PatternMatching::UNBOUND) { 'two args' }
50
+ subject.defn(:initialize, PatternMatching::UNBOUND) { 'one arg' }
51
+
52
+ lambda { subject.new(1) }.should_not raise_error
53
+ lambda { subject.new(1, 2) }.should_not raise_error
54
+ lambda { subject.new(1, 2, 3) }.should_not raise_error
55
+ lambda { subject.new(1, 2, 3, 4) }.should raise_error
56
+ end
55
57
  end
56
58
  end
57
59
 
@@ -0,0 +1,310 @@
1
+ require 'spec_helper'
2
+ require_relative 'obligation_shared'
3
+
4
+ module Functional
5
+
6
+ describe Promise do
7
+
8
+ let!(:fulfilled_value) { 10 }
9
+ let!(:rejected_reason) { StandardError.new('mojo jojo') }
10
+
11
+ let(:pending_subject) do
12
+ Promise.new{ sleep(1) }
13
+ end
14
+
15
+ let(:fulfilled_subject) do
16
+ Promise.new{ fulfilled_value }.tap(){ sleep(0.1) }
17
+ end
18
+
19
+ let(:rejected_subject) do
20
+ Promise.new{ raise rejected_reason }.
21
+ rescue{ nil }.tap(){ sleep(0.1) }
22
+ end
23
+
24
+ before(:each) do
25
+ $GLOBAL_THREAD_POOL = CachedThreadPool.new
26
+ end
27
+
28
+ it_should_behave_like Obligation
29
+
30
+ context 'behavior' do
31
+
32
+ it 'implements :promise behavior' do
33
+ lambda {
34
+ Promise.new{ nil }
35
+ }.should_not raise_error(BehaviorError)
36
+
37
+ Promise.new{ nil }.behaves_as?(:promise).should be_true
38
+ end
39
+
40
+ it 'implements :future behavior' do
41
+ lambda {
42
+ Promise.new{ nil }
43
+ }.should_not raise_error(BehaviorError)
44
+
45
+ Promise.new{ nil }.behaves_as?(:future).should be_true
46
+ end
47
+ end
48
+
49
+ context '#then' do
50
+
51
+ it 'returns a new Promise when :pending' do
52
+ p1 = pending_subject
53
+ p2 = p1.then{}
54
+ p2.should be_a(Promise)
55
+ p1.should_not eq p2
56
+ end
57
+
58
+ it 'returns a new Promise when :fulfilled' do
59
+ p1 = fulfilled_subject
60
+ p2 = p1.then{}
61
+ p2.should be_a(Promise)
62
+ p1.should_not eq p2
63
+ end
64
+
65
+ it 'returns a new Promise when :rejected' do
66
+ p1 = rejected_subject
67
+ p2 = p1.then{}
68
+ p2.should be_a(Promise)
69
+ p1.should_not eq p2
70
+ end
71
+
72
+ it 'immediately rejects new promises when self has been rejected' do
73
+ p = rejected_subject
74
+ p.then.should be_rejected
75
+ end
76
+
77
+ it 'accepts a nil block' do
78
+ lambda {
79
+ pending_subject.then
80
+ }.should_not raise_error
81
+ end
82
+
83
+ it 'can be called more than once' do
84
+ p = pending_subject
85
+ p1 = p.then{}
86
+ p2 = p.then{}
87
+ p1.object_id.should_not eq p2.object_id
88
+ end
89
+ end
90
+
91
+ context '#rescue' do
92
+
93
+ it 'returns self when a block is given' do
94
+ p1 = pending_subject
95
+ p2 = p1.rescue{}
96
+ p1.object_id.should eq p2.object_id
97
+ end
98
+
99
+ it 'returns self when no block is given' do
100
+ p1 = pending_subject
101
+ p2 = p1.rescue
102
+ p1.object_id.should eq p2.object_id
103
+ end
104
+
105
+ it 'accepts an exception class as the first parameter' do
106
+ lambda {
107
+ pending_subject.rescue(StandardError){}
108
+ }.should_not raise_error
109
+ end
110
+ end
111
+
112
+ context 'fulfillment' do
113
+
114
+ it 'passes all arguments to the first promise in the chain' do
115
+ @a = @b = @c = nil
116
+ p = Promise.new(1, 2, 3) do |a, b, c|
117
+ @a, @b, @c = a, b, c
118
+ end
119
+ sleep(0.1)
120
+ [@a, @b, @c].should eq [1, 2, 3]
121
+ end
122
+
123
+ it 'passes the result of each block to all its children' do
124
+ @expected = nil
125
+ Promise.new(10){|a| a * 2 }.then{|result| @expected = result}
126
+ sleep(0.1)
127
+ @expected.should eq 20
128
+ end
129
+
130
+ it 'sets the promise value to the result if its block' do
131
+ p = Promise.new(10){|a| a * 2 }.then{|result| result * 2}
132
+ sleep(0.1)
133
+ p.value.should eq 40
134
+ end
135
+
136
+ it 'sets the promise state to :fulfilled if the block completes' do
137
+ p = Promise.new(10){|a| a * 2 }.then{|result| result * 2}
138
+ sleep(0.1)
139
+ p.should be_fulfilled
140
+ end
141
+
142
+ it 'passes the last result through when a promise has no block' do
143
+ @expected = nil
144
+ Promise.new(10){|a| a * 2 }.then.then{|result| @expected = result}
145
+ sleep(0.1)
146
+ @expected.should eq 20
147
+ end
148
+ end
149
+
150
+ context 'rejection' do
151
+
152
+ it 'sets the promise reason the error object on exception' do
153
+ p = Promise.new{ raise StandardError.new('Boom!') }
154
+ sleep(0.1)
155
+ p.reason.should be_a(Exception)
156
+ p.reason.should.to_s =~ /Boom!/
157
+ end
158
+
159
+ it 'sets the promise state to :rejected on exception' do
160
+ p = Promise.new{ raise StandardError.new('Boom!') }
161
+ sleep(0.1)
162
+ p.should be_rejected
163
+ end
164
+
165
+ it 'recursively rejects all children' do
166
+ p = Promise.new{ Thread.pass; raise StandardError.new('Boom!') }
167
+ promises = 10.times.collect{ p.then{ true } }
168
+ sleep(0.1)
169
+
170
+ 10.times.each{|i| promises[i].should be_rejected }
171
+ end
172
+
173
+ it 'skips processing rejected promises' do
174
+ p = Promise.new{ raise StandardError.new('Boom!') }
175
+ promises = 3.times.collect{ p.then{ true } }
176
+ sleep(0.1)
177
+ promises.each{|p| p.value.should_not be_true }
178
+ end
179
+
180
+ it 'calls the first exception block with a matching class' do
181
+ @expected = nil
182
+ Promise.new{ raise StandardError }.
183
+ rescue(StandardError){|ex| @expected = 1 }.
184
+ rescue(StandardError){|ex| @expected = 2 }.
185
+ rescue(StandardError){|ex| @expected = 3 }
186
+ sleep(0.1)
187
+ @expected.should eq 1
188
+ end
189
+
190
+ it 'matches all with a rescue with no class given' do
191
+ @expected = nil
192
+ Promise.new{ raise NoMethodError }.
193
+ rescue(LoadError){|ex| @expected = 1 }.
194
+ rescue{|ex| @expected = 2 }.
195
+ rescue(StandardError){|ex| @expected = 3 }
196
+ sleep(0.1)
197
+ @expected.should eq 2
198
+ end
199
+
200
+ it 'searches associated rescue handlers in order' do
201
+ @expected = nil
202
+ Promise.new{ raise ArgumentError }.
203
+ rescue(ArgumentError){|ex| @expected = 1 }.
204
+ rescue(LoadError){|ex| @expected = 2 }.
205
+ rescue(Exception){|ex| @expected = 3 }
206
+ sleep(0.1)
207
+ @expected.should eq 1
208
+
209
+ @expected = nil
210
+ Promise.new{ raise LoadError }.
211
+ rescue(ArgumentError){|ex| @expected = 1 }.
212
+ rescue(LoadError){|ex| @expected = 2 }.
213
+ rescue(Exception){|ex| @expected = 3 }
214
+ sleep(0.1)
215
+ @expected.should eq 2
216
+
217
+ @expected = nil
218
+ Promise.new{ raise StandardError }.
219
+ rescue(ArgumentError){|ex| @expected = 1 }.
220
+ rescue(LoadError){|ex| @expected = 2 }.
221
+ rescue(Exception){|ex| @expected = 3 }
222
+ sleep(0.1)
223
+ @expected.should eq 3
224
+ end
225
+
226
+ it 'passes the exception object to the matched block' do
227
+ @expected = nil
228
+ Promise.new{ raise StandardError }.
229
+ rescue(ArgumentError){|ex| @expected = ex }.
230
+ rescue(LoadError){|ex| @expected = ex }.
231
+ rescue(Exception){|ex| @expected = ex }
232
+ sleep(0.1)
233
+ @expected.should be_a(StandardError)
234
+ end
235
+
236
+ it 'ignores rescuers without a block' do
237
+ @expected = nil
238
+ Promise.new{ raise StandardError }.
239
+ rescue(StandardError).
240
+ rescue(StandardError){|ex| @expected = ex }.
241
+ rescue(Exception){|ex| @expected = ex }
242
+ sleep(0.1)
243
+ @expected.should be_a(StandardError)
244
+ end
245
+
246
+ it 'supresses the exception if no rescue matches' do
247
+ lambda {
248
+ Promise.new{ raise StandardError }.
249
+ rescue(ArgumentError){|ex| @expected = ex }.
250
+ rescue(StandardError){|ex| @expected = ex }.
251
+ rescue(Exception){|ex| @expected = ex }
252
+ sleep(0.1)
253
+ }.should_not raise_error
254
+ end
255
+
256
+ it 'supresses exceptions thrown from rescue handlers' do
257
+ lambda {
258
+ Promise.new{ raise ArgumentError }.
259
+ rescue(Exception){ raise StandardError }
260
+ sleep(0.1)
261
+ }.should_not raise_error(StandardError)
262
+ end
263
+
264
+ it 'calls matching rescue handlers on all children' do
265
+ @expected = []
266
+ Promise.new{ Thread.pass; raise StandardError }.
267
+ then{ sleep(0.1) }.rescue{ @expected << 'Boom!' }.
268
+ then{ sleep(0.1) }.rescue{ @expected << 'Boom!' }.
269
+ then{ sleep(0.1) }.rescue{ @expected << 'Boom!' }.
270
+ then{ sleep(0.1) }.rescue{ @expected << 'Boom!' }.
271
+ then{ sleep(0.1) }.rescue{ @expected << 'Boom!' }
272
+ sleep(0.1)
273
+
274
+ @expected.length.should eq 5
275
+ end
276
+ end
277
+
278
+ context 'aliases' do
279
+
280
+ it 'aliases #realized? for #fulfilled?' do
281
+ fulfilled_subject.should be_realized
282
+ end
283
+
284
+ it 'aliases #deref for #value' do
285
+ fulfilled_subject.deref.should eq fulfilled_value
286
+ end
287
+
288
+ it 'aliases #catch for #rescue' do
289
+ @expected = nil
290
+ Promise.new{ raise StandardError }.catch{ @expected = true }
291
+ sleep(0.1)
292
+ @expected.should be_true
293
+ end
294
+
295
+ it 'aliases #on_error for #rescue' do
296
+ @expected = nil
297
+ Promise.new{ raise StandardError }.on_error{ @expected = true }
298
+ sleep(0.1)
299
+ @expected.should be_true
300
+ end
301
+
302
+ it 'aliases Kernel#promise for Promise.new' do
303
+ promise().should be_a(Promise)
304
+ promise(){ nil }.should be_a(Promise)
305
+ promise(1, 2, 3).should be_a(Promise)
306
+ promise(1, 2, 3){ nil }.should be_a(Promise)
307
+ end
308
+ end
309
+ end
310
+ end