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
@@ -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