functional-ruby 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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