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
@@ -1,37 +1,16 @@
1
1
  require 'spec_helper'
2
2
 
3
- #behaviour_info(:gen_foo, foo: 0, bar: 1, baz: 2, boom: -1, bam: :any)
4
-
5
- #class Foo
6
- #behavior(:gen_foo)
7
-
8
- #def foo
9
- #return 'foo/0'
10
- #end
11
-
12
- #def bar(one, &block)
13
- #return 'bar/1'
14
- #end
15
-
16
- #def baz(one, two)
17
- #return 'baz/2'
18
- #end
19
-
20
- #def boom(*args)
21
- #return 'boom/-1'
22
- #end
23
-
24
- #def bam
25
- #return 'bam!'
26
- #end
27
- #end
28
-
29
- describe 'behavior/interface definitions' do
3
+ describe '-behavior' do
30
4
 
31
5
  before(:each) do
6
+ @__behavior_info__ = $__behavior_info__
32
7
  $__behavior_info__ = {}
33
8
  end
34
9
 
10
+ after(:each) do
11
+ $__behavior_info__ = @__behavior_info__
12
+ end
13
+
35
14
  context 'behavior_info/2' do
36
15
 
37
16
  it 'accepts a symbol name' do
@@ -77,7 +56,7 @@ describe 'behavior/interface definitions' do
77
56
  Class.new{
78
57
  behavior(:gen_foo)
79
58
  }
80
- }.should raise_error(ArgumentError)
59
+ }.should raise_error(BehaviorError)
81
60
  end
82
61
 
83
62
  it 'can be called multiple times for one class' do
@@ -93,7 +72,7 @@ describe 'behavior/interface definitions' do
93
72
  end
94
73
  end
95
74
 
96
- context 'behavior check on object creation' do
75
+ context 'object creation' do
97
76
 
98
77
  it 'raises an exception when one or more function definitions are missing' do
99
78
  behavior_info(:gen_foo, foo: 0, bar: 1)
@@ -104,7 +83,7 @@ describe 'behavior/interface definitions' do
104
83
 
105
84
  lambda {
106
85
  clazz.new
107
- }.should raise_error(ArgumentError)
86
+ }.should raise_error(BehaviorError)
108
87
  end
109
88
 
110
89
  it 'raises an exception when one or more functions do not have proper arity' do
@@ -116,7 +95,7 @@ describe 'behavior/interface definitions' do
116
95
 
117
96
  lambda {
118
97
  clazz.new
119
- }.should raise_error(ArgumentError)
98
+ }.should raise_error(BehaviorError)
120
99
  end
121
100
 
122
101
  it 'accepts any arity when function arity is set to :any' do
@@ -128,7 +107,7 @@ describe 'behavior/interface definitions' do
128
107
 
129
108
  lambda {
130
109
  clazz.new
131
- }.should_not raise_error(ArgumentError)
110
+ }.should_not raise_error(BehaviorError)
132
111
  end
133
112
 
134
113
  it 'creates the object when function definitions match' do
@@ -141,7 +120,7 @@ describe 'behavior/interface definitions' do
141
120
 
142
121
  lambda {
143
122
  clazz.new
144
- }.should_not raise_error(ArgumentError)
123
+ }.should_not raise_error(BehaviorError)
145
124
  end
146
125
  end
147
126
 
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+ require_relative 'thread_pool_shared'
3
+
4
+ module Functional
5
+
6
+ describe CachedThreadPool do
7
+
8
+ subject { CachedThreadPool.new }
9
+
10
+ it_should_behave_like 'Thread Pool'
11
+
12
+ context '#initialize' do
13
+ it 'aliases Functional#new_cached_thread_pool' do
14
+ pool = Functional.new_cached_thread_pool
15
+ pool.should be_a(CachedThreadPool)
16
+ pool.size.should eq 0
17
+ end
18
+ end
19
+
20
+ context '#kill' do
21
+
22
+ it 'kills all threads' do
23
+ Thread.should_receive(:kill).exactly(5).times
24
+ pool = CachedThreadPool.new
25
+ 5.times{ sleep(0.1); pool << proc{ sleep(1) } }
26
+ sleep(1)
27
+ pool.kill
28
+ sleep(0.1)
29
+ end
30
+ end
31
+
32
+ context '#size' do
33
+
34
+ it 'returns zero for a new thread pool' do
35
+ subject.size.should eq 0
36
+ end
37
+
38
+ it 'returns the size of the subject when running' do
39
+ 5.times{ sleep(0.1); subject << proc{ sleep(1) } }
40
+ subject.size.should eq 5
41
+ end
42
+
43
+ it 'returns zero once shut down' do
44
+ subject.shutdown
45
+ subject.size.should eq 0
46
+ end
47
+ end
48
+
49
+ context 'worker creation and caching' do
50
+
51
+ it 'creates new workers when there are none available' do
52
+ subject.size.should eq 0
53
+ 5.times{ sleep(0.1); subject << proc{ sleep(1000) } }
54
+ sleep(1)
55
+ subject.size.should eq 5
56
+ end
57
+
58
+ it 'uses existing idle threads' do
59
+ 5.times{ sleep(0.05); subject << proc{ sleep(0.5) } }
60
+ sleep(1)
61
+ 3.times{ sleep(0.1); subject << proc{ sleep(0.5) } }
62
+ subject.size.should eq 5
63
+ end
64
+ end
65
+
66
+ context 'garbage collection' do
67
+
68
+ subject{ CachedThreadPool.new(gc_interval: 1, thread_idleime: 1) }
69
+
70
+ it 'starts when the first thread is added to the pool' do
71
+ subject.should_receive(:collect_garbage)
72
+ subject << proc{ nil }
73
+ sleep(0.1)
74
+ end
75
+
76
+ it 'removes from pool any thread that has been idle too long' do
77
+ subject << proc{ nil }
78
+ subject.size.should eq 1
79
+ sleep(1.5)
80
+ subject.size.should eq 0
81
+ end
82
+
83
+ it 'removed from pool any dead thread' do
84
+ subject << proc{ raise StandardError }
85
+ subject.size.should eq 1
86
+ sleep(1.5)
87
+ subject.size.should eq 0
88
+ end
89
+
90
+ it 'resets the working count appropriately' do
91
+ subject << proc{ sleep(1000) }
92
+ sleep(0.1)
93
+ subject << proc{ raise StandardError }
94
+ sleep(0.1)
95
+ subject << proc{ nil }
96
+
97
+ sleep(0.1)
98
+ subject.working.should eq 2
99
+
100
+ sleep(1.5)
101
+ subject.working.should eq 1
102
+ end
103
+
104
+ it 'stops collection when the pool size becomes zero' do
105
+ 3.times{ sleep(0.1); subject << proc{ sleep(0.5) } }
106
+ subject.instance_variable_get(:@collector).status.should eq 'sleep'
107
+ sleep(1.5)
108
+ subject.instance_variable_get(:@collector).status.should be_false
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ module Functional
4
+
5
+ describe 'concurrency' do
6
+
7
+ context '#go' do
8
+
9
+ before(:each) do
10
+ $GLOBAL_THREAD_POOL = CachedThreadPool.new
11
+ end
12
+
13
+ it 'passes all arguments to the block' do
14
+ @expected = nil
15
+ go(1, 2, 3){|a, b, c| @expected = [c, b, a] }
16
+ sleep(0.1)
17
+ @expected.should eq [3, 2, 1]
18
+ end
19
+
20
+ it 'returns true if the thread is successfully created' do
21
+ $GLOBAL_THREAD_POOL.should_receive(:post).and_return(true)
22
+ go{ nil }.should be_true
23
+ end
24
+
25
+ it 'returns false if the thread cannot be created' do
26
+ $GLOBAL_THREAD_POOL.should_receive(:post).and_return(false)
27
+ go{ nil }.should be_false
28
+ end
29
+
30
+ it 'immediately returns false if no block is given' do
31
+ go().should be_false
32
+ end
33
+
34
+ it 'does not create a thread if no block is given' do
35
+ $GLOBAL_THREAD_POOL.should_not_receive(:post)
36
+ go()
37
+ sleep(0.1)
38
+ end
39
+
40
+ it 'supresses exceptions on the thread' do
41
+ lambda{
42
+ go{ raise StandardError }
43
+ sleep(0.1)
44
+ }.should_not raise_error
45
+ end
46
+
47
+ it 'processes the block' do
48
+ @expected = false
49
+ go(1,2,3){|*args| @expected = args }
50
+ sleep(0.1)
51
+ @expected.should eq [1,2,3]
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,246 @@
1
+ require 'spec_helper'
2
+
3
+ require 'functional/agent'
4
+ require 'functional/future'
5
+ require 'functional/promise'
6
+
7
+ module Functional
8
+
9
+ describe EventMachineDeferProxy do
10
+
11
+ subject { EventMachineDeferProxy.new }
12
+
13
+ context '#post' do
14
+
15
+ it 'proxies a call without arguments' do
16
+ @expected = false
17
+ EventMachine.run do
18
+ subject.post{ @expected = true }
19
+ sleep(0.1)
20
+ EventMachine.stop
21
+ end
22
+ @expected.should eq true
23
+ end
24
+
25
+ it 'proxies a call with arguments' do
26
+ @expected = []
27
+ EventMachine.run do
28
+ subject.post(1,2,3){|*args| @expected = args }
29
+ sleep(0.1)
30
+ EventMachine.stop
31
+ end
32
+ @expected.should eq [1,2,3]
33
+ end
34
+
35
+ it 'aliases #<<' do
36
+ @expected = false
37
+ EventMachine.run do
38
+ subject << proc{ @expected = true }
39
+ sleep(0.1)
40
+ EventMachine.stop
41
+ end
42
+ @expected.should eq true
43
+ end
44
+ end
45
+
46
+ context 'operation' do
47
+
48
+ context 'goroutine' do
49
+
50
+ it 'passes all arguments to the block' do
51
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
52
+
53
+ EventMachine.run do
54
+
55
+ @expected = nil
56
+ go(1, 2, 3){|a, b, c| @expected = [c, b, a] }
57
+ sleep(0.1)
58
+ @expected.should eq [3, 2, 1]
59
+
60
+ EventMachine.stop
61
+ end
62
+ end
63
+ end
64
+
65
+ context Agent do
66
+
67
+ subject { Agent.new(0) }
68
+
69
+ it 'supports fulfillment' do
70
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
71
+
72
+ EventMachine.run do
73
+
74
+ @expected = []
75
+ subject.post{ @expected << 1 }
76
+ subject.post{ @expected << 2 }
77
+ subject.post{ @expected << 3 }
78
+ sleep(0.1)
79
+ @expected.should eq [1,2,3]
80
+
81
+ EventMachine.stop
82
+ end
83
+ end
84
+
85
+ it 'supports validation' do
86
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
87
+
88
+ EventMachine.run do
89
+
90
+ @expected = nil
91
+ subject.validate{ @expected = 10; true }
92
+ subject.post{ nil }
93
+ sleep(0.1)
94
+ @expected.should eq 10
95
+
96
+ EventMachine.stop
97
+ end
98
+ end
99
+
100
+ it 'supports rejection' do
101
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
102
+
103
+ EventMachine.run do
104
+
105
+ @expected = nil
106
+ subject.
107
+ on_error(StandardError){|ex| @expected = 1 }.
108
+ on_error(StandardError){|ex| @expected = 2 }.
109
+ on_error(StandardError){|ex| @expected = 3 }
110
+ subject.post{ raise StandardError }
111
+ sleep(0.1)
112
+ @expected.should eq 1
113
+
114
+ EventMachine.stop
115
+ end
116
+ end
117
+ end
118
+
119
+ context Future do
120
+
121
+ it 'supports fulfillment' do
122
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
123
+
124
+ EventMachine.run do
125
+
126
+ @a = @b = @c = nil
127
+ f = Future.new(1, 2, 3) do |a, b, c|
128
+ @a, @b, @c = a, b, c
129
+ end
130
+ sleep(0.1)
131
+ [@a, @b, @c].should eq [1, 2, 3]
132
+
133
+ sleep(0.1)
134
+ EventMachine.stop
135
+ end
136
+ end
137
+ end
138
+
139
+ context Promise do
140
+
141
+ context 'fulfillment' do
142
+
143
+ it 'passes all arguments to the first promise in the chain' do
144
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
145
+
146
+ EventMachine.run do
147
+
148
+ @a = @b = @c = nil
149
+ p = Promise.new(1, 2, 3) do |a, b, c|
150
+ @a, @b, @c = a, b, c
151
+ end
152
+ sleep(0.1)
153
+ [@a, @b, @c].should eq [1, 2, 3]
154
+
155
+ sleep(0.1)
156
+ EventMachine.stop
157
+ end
158
+ end
159
+
160
+ it 'passes the result of each block to all its children' do
161
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
162
+
163
+ EventMachine.run do
164
+ @expected = nil
165
+ Promise.new(10){|a| a * 2 }.then{|result| @expected = result}
166
+ sleep(0.1)
167
+ @expected.should eq 20
168
+
169
+ sleep(0.1)
170
+ EventMachine.stop
171
+ end
172
+ end
173
+
174
+ it 'sets the promise value to the result if its block' do
175
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
176
+
177
+ EventMachine.run do
178
+
179
+ p = Promise.new(10){|a| a * 2 }.then{|result| result * 2}
180
+ sleep(0.1)
181
+ p.value.should eq 40
182
+
183
+ sleep(0.1)
184
+ EventMachine.stop
185
+ end
186
+ end
187
+ end
188
+
189
+ context 'rejection' do
190
+
191
+ it 'sets the promise reason and error on exception' do
192
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
193
+
194
+ EventMachine.run do
195
+
196
+ p = Promise.new{ raise StandardError.new('Boom!') }
197
+ sleep(0.1)
198
+ p.reason.should be_a(Exception)
199
+ p.reason.should.to_s =~ /Boom!/
200
+ p.should be_rejected
201
+
202
+ sleep(0.1)
203
+ EventMachine.stop
204
+ end
205
+ end
206
+
207
+ it 'calls the first exception block with a matching class' do
208
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
209
+
210
+ EventMachine.run do
211
+
212
+ @expected = nil
213
+ Promise.new{ raise StandardError }.
214
+ on_error(StandardError){|ex| @expected = 1 }.
215
+ on_error(StandardError){|ex| @expected = 2 }.
216
+ on_error(StandardError){|ex| @expected = 3 }
217
+ sleep(0.1)
218
+ @expected.should eq 1
219
+
220
+ sleep(0.1)
221
+ EventMachine.stop
222
+ end
223
+ end
224
+
225
+ it 'passes the exception object to the matched block' do
226
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
227
+
228
+ EventMachine.run do
229
+
230
+ @expected = nil
231
+ Promise.new{ raise StandardError }.
232
+ on_error(ArgumentError){|ex| @expected = ex }.
233
+ on_error(LoadError){|ex| @expected = ex }.
234
+ on_error(Exception){|ex| @expected = ex }
235
+ sleep(0.1)
236
+ @expected.should be_a(StandardError)
237
+
238
+ sleep(0.1)
239
+ EventMachine.stop
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,114 @@
1
+ require 'spec_helper'
2
+
3
+ module Functional
4
+
5
+ describe Event do
6
+
7
+ subject{ Event.new }
8
+
9
+ context '#initialize' do
10
+
11
+ it 'sets the state to unset' do
12
+ subject.should_not be_set
13
+ end
14
+ end
15
+
16
+ context '#set?' do
17
+
18
+ it 'returns true when the event has been set' do
19
+ subject.set
20
+ subject.should be_set
21
+ end
22
+
23
+ it 'returns false if the event is unset' do
24
+ subject.reset
25
+ subject.should_not be_set
26
+ end
27
+ end
28
+
29
+ context '#set' do
30
+
31
+ it 'triggers the event' do
32
+ subject.reset
33
+ @expected = false
34
+ Thread.new{ sleep(0.5); subject.wait; @expected = true }
35
+ subject.set
36
+ sleep(1)
37
+ @expected.should be_true
38
+ end
39
+
40
+ it 'sets the state to set' do
41
+ subject.set
42
+ subject.should be_set
43
+ end
44
+ end
45
+
46
+ context '#reset' do
47
+
48
+ it 'sets the state to unset' do
49
+ subject.set
50
+ subject.should be_set
51
+ subject.reset
52
+ subject.should_not be_set
53
+ end
54
+ end
55
+
56
+ context '#wait' do
57
+
58
+ it 'returns immediately when the event has been set' do
59
+ subject.reset
60
+ @expected = false
61
+ subject.set
62
+ Thread.new{ subject.wait(1000); @expected = true}
63
+ sleep(1)
64
+ @expected.should be_true
65
+ end
66
+
67
+ it 'returns true once the event is set' do
68
+ subject.set
69
+ subject.wait.should be_true
70
+ end
71
+
72
+ it 'blocks indefinitely when the timer is nil' do
73
+ subject.reset
74
+ @expected = false
75
+ Thread.new{ subject.wait; @expected = true}
76
+ subject.set
77
+ sleep(1)
78
+ @expected.should be_true
79
+ end
80
+
81
+ it 'stops waiting when the timer expires' do
82
+ subject.reset
83
+ @expected = false
84
+ Thread.new{ subject.wait(0.5); @expected = true}
85
+ sleep(1)
86
+ @expected.should be_true
87
+ end
88
+
89
+ it 'returns false when the timer expires' do
90
+ subject.reset
91
+ subject.wait(1).should be_false
92
+ end
93
+
94
+ it 'triggers multiple waiting threads' do
95
+ subject.reset
96
+ @expected = []
97
+ 5.times{ Thread.new{ subject.wait; @expected << Thread.current.object_id } }
98
+ subject.set
99
+ sleep(1)
100
+ @expected.length.should eq 5
101
+ end
102
+
103
+ it 'behaves appropriately if wait begins while #set is processing' do
104
+ subject.reset
105
+ @expected = []
106
+ 5.times{ Thread.new{ subject.wait(5) } }
107
+ subject.set
108
+ 5.times{ Thread.new{ subject.wait; @expected << Thread.current.object_id } }
109
+ sleep(1)
110
+ @expected.length.should eq 5
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,84 @@
1
+ require 'spec_helper'
2
+ require_relative 'thread_pool_shared'
3
+
4
+ module Functional
5
+
6
+ describe FixedThreadPool do
7
+
8
+ subject { FixedThreadPool.new(5) }
9
+
10
+ it_should_behave_like 'Thread Pool'
11
+
12
+ context '#initialize' do
13
+
14
+ it 'raises an exception when the pool size is less than one' do
15
+ lambda {
16
+ FixedThreadPool.new(0)
17
+ }.should raise_error(ArgumentError)
18
+ end
19
+
20
+ it 'raises an exception when the pool size is greater than 1024' do
21
+ lambda {
22
+ FixedThreadPool.new(1025)
23
+ }.should raise_error(ArgumentError)
24
+ end
25
+
26
+ it 'creates a thread pool of the given size' do
27
+ thread = mock('thread')
28
+ # add one for the garbage collector
29
+ Thread.should_receive(:new).exactly(5+1).times.and_return(thread)
30
+ pool = FixedThreadPool.new(5)
31
+ pool.size.should eq 5
32
+ end
33
+
34
+ it 'aliases Functional#new_fixed_thread_pool' do
35
+ pool = Functional.new_fixed_thread_pool(5)
36
+ pool.should be_a(FixedThreadPool)
37
+ pool.size.should eq 5
38
+ end
39
+ end
40
+
41
+ context '#kill' do
42
+
43
+ it 'kills all threads' do
44
+ Thread.should_receive(:kill).exactly(5).times
45
+ pool = FixedThreadPool.new(5)
46
+ pool.kill
47
+ sleep(0.1)
48
+ end
49
+ end
50
+
51
+ context '#size' do
52
+
53
+ let(:pool_size) { 3 }
54
+ subject { FixedThreadPool.new(pool_size) }
55
+
56
+ it 'returns the size of the subject when running' do
57
+ subject.size.should eq pool_size
58
+ end
59
+
60
+ it 'returns zero while shutting down' do
61
+ subject.post{ sleep(1) }
62
+ subject.shutdown
63
+ subject.size.should eq 0
64
+ end
65
+
66
+ it 'returns zero once shut down' do
67
+ subject.shutdown
68
+ subject.size.should eq 0
69
+ end
70
+ end
71
+
72
+ context 'exception handling' do
73
+
74
+ it 'restarts threads that experience exception' do
75
+ pool = FixedThreadPool.new(5)
76
+ 3.times{ pool << proc{ raise StandardError } }
77
+ sleep(2)
78
+ pool.size.should eq 5
79
+ pool.status.should_not include(nil)
80
+ #pool.status.include?(nil).should be_false
81
+ end
82
+ end
83
+ end
84
+ end