functional-ruby 0.7.7 → 1.0.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +92 -152
  3. data/doc/memo.txt +192 -0
  4. data/doc/pattern_matching.txt +485 -0
  5. data/doc/protocol.txt +221 -0
  6. data/doc/record.txt +144 -0
  7. data/doc/thread_safety.txt +8 -0
  8. data/lib/functional.rb +48 -18
  9. data/lib/functional/abstract_struct.rb +161 -0
  10. data/lib/functional/delay.rb +117 -0
  11. data/lib/functional/either.rb +222 -0
  12. data/lib/functional/memo.rb +93 -0
  13. data/lib/functional/method_signature.rb +72 -0
  14. data/lib/functional/option.rb +209 -0
  15. data/lib/functional/pattern_matching.rb +117 -100
  16. data/lib/functional/protocol.rb +157 -0
  17. data/lib/functional/protocol_info.rb +193 -0
  18. data/lib/functional/record.rb +155 -0
  19. data/lib/functional/type_check.rb +112 -0
  20. data/lib/functional/union.rb +152 -0
  21. data/lib/functional/version.rb +3 -1
  22. data/spec/functional/abstract_struct_shared.rb +154 -0
  23. data/spec/functional/complex_pattern_matching_spec.rb +205 -0
  24. data/spec/functional/configuration_spec.rb +17 -0
  25. data/spec/functional/delay_spec.rb +147 -0
  26. data/spec/functional/either_spec.rb +237 -0
  27. data/spec/functional/memo_spec.rb +207 -0
  28. data/spec/functional/option_spec.rb +292 -0
  29. data/spec/functional/pattern_matching_spec.rb +279 -276
  30. data/spec/functional/protocol_info_spec.rb +444 -0
  31. data/spec/functional/protocol_spec.rb +274 -0
  32. data/spec/functional/record_spec.rb +175 -0
  33. data/spec/functional/type_check_spec.rb +103 -0
  34. data/spec/functional/union_spec.rb +110 -0
  35. data/spec/spec_helper.rb +6 -4
  36. metadata +55 -45
  37. data/lib/functional/behavior.rb +0 -138
  38. data/lib/functional/behaviour.rb +0 -2
  39. data/lib/functional/catalog.rb +0 -487
  40. data/lib/functional/collection.rb +0 -403
  41. data/lib/functional/inflect.rb +0 -127
  42. data/lib/functional/platform.rb +0 -120
  43. data/lib/functional/search.rb +0 -132
  44. data/lib/functional/sort.rb +0 -41
  45. data/lib/functional/utilities.rb +0 -189
  46. data/md/behavior.md +0 -188
  47. data/md/catalog.md +0 -32
  48. data/md/collection.md +0 -32
  49. data/md/inflect.md +0 -32
  50. data/md/pattern_matching.md +0 -512
  51. data/md/platform.md +0 -32
  52. data/md/search.md +0 -32
  53. data/md/sort.md +0 -32
  54. data/md/utilities.md +0 -55
  55. data/spec/functional/behavior_spec.rb +0 -528
  56. data/spec/functional/catalog_spec.rb +0 -1206
  57. data/spec/functional/collection_spec.rb +0 -752
  58. data/spec/functional/inflect_spec.rb +0 -85
  59. data/spec/functional/integration_spec.rb +0 -205
  60. data/spec/functional/platform_spec.rb +0 -501
  61. data/spec/functional/search_spec.rb +0 -187
  62. data/spec/functional/sort_spec.rb +0 -61
  63. data/spec/functional/utilities_spec.rb +0 -277
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ module Functional
4
+
5
+ context Configuration do
6
+
7
+ it 'configures the gem' do
8
+ cfg = nil
9
+ Functional.configure do |config|
10
+ cfg = config
11
+ end
12
+
13
+ expect(cfg).to be_a Configuration
14
+ expect(cfg).to eq Functional.configuration
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,147 @@
1
+ require 'spec_helper'
2
+
3
+ module Functional
4
+
5
+ describe Delay do
6
+
7
+ let!(:fulfilled_value) { 10 }
8
+ let!(:rejected_reason) { StandardError.new('mojo jojo') }
9
+
10
+ let(:pending_subject) do
11
+ Delay.new{ fulfilled_value }
12
+ end
13
+
14
+ let(:fulfilled_subject) do
15
+ delay = Delay.new{ fulfilled_value }
16
+ delay.tap{ delay.value }
17
+ end
18
+
19
+ let(:rejected_subject) do
20
+ delay = Delay.new{ raise rejected_reason }
21
+ delay.tap{ delay.value }
22
+ end
23
+
24
+ specify{ Functional::Protocol::Satisfy! Delay, :Disposition }
25
+
26
+ context '#initialize' do
27
+
28
+ it 'sets the state to :pending' do
29
+ expect(Delay.new{ nil }.state).to eq :pending
30
+ expect(Delay.new{ nil }).to be_pending
31
+ end
32
+
33
+ it 'raises an exception when no block given' do
34
+ expect {
35
+ Delay.new
36
+ }.to raise_error(ArgumentError)
37
+ end
38
+ end
39
+
40
+ context '#state' do
41
+
42
+ it 'is :pending when first created' do
43
+ f = pending_subject
44
+ expect(f.state).to eq(:pending)
45
+ expect(f).to be_pending
46
+ end
47
+
48
+ it 'is :fulfilled when the handler completes' do
49
+ f = fulfilled_subject
50
+ expect(f.state).to eq(:fulfilled)
51
+ expect(f).to be_fulfilled
52
+ end
53
+
54
+ it 'is :rejected when the handler raises an exception' do
55
+ f = rejected_subject
56
+ expect(f.state).to eq(:rejected)
57
+ expect(f).to be_rejected
58
+ end
59
+ end
60
+
61
+ context '#value' do
62
+
63
+ let(:task){ proc{ nil } }
64
+
65
+ it 'blocks the caller when :pending and timeout is nil' do
66
+ f = pending_subject
67
+ expect(f.value).to be_truthy
68
+ expect(f).to be_fulfilled
69
+ end
70
+
71
+ it 'is nil when :rejected' do
72
+ expected = rejected_subject.value
73
+ expect(expected).to be_nil
74
+ end
75
+
76
+ it 'is set to the return value of the block when :fulfilled' do
77
+ expected = fulfilled_subject.value
78
+ expect(expected).to eq fulfilled_value
79
+ end
80
+
81
+ it 'does not call the block before #value is called' do
82
+ expect(task).not_to receive(:call).with(any_args)
83
+ Delay.new(&task)
84
+ end
85
+
86
+ it 'calls the block when #value is called' do
87
+ expect(task).to receive(:call).once.with(any_args).and_return(nil)
88
+ Delay.new(&task).value
89
+ end
90
+
91
+ it 'only calls the block once no matter how often #value is called' do
92
+ expect(task).to receive(:call).once.with(any_args).and_return(nil)
93
+ delay = Delay.new(&task)
94
+ 5.times{ delay.value }
95
+ end
96
+ end
97
+
98
+ context '#reason' do
99
+
100
+ it 'is nil when :pending' do
101
+ expect(pending_subject.reason).to be_nil
102
+ end
103
+
104
+ it 'is nil when :fulfilled' do
105
+ expect(fulfilled_subject.reason).to be_nil
106
+ end
107
+
108
+ it 'is set to error object of the exception when :rejected' do
109
+ expect(rejected_subject.reason).to be_a(Exception)
110
+ expect(rejected_subject.reason.to_s).to match(/#{rejected_reason}/)
111
+ end
112
+ end
113
+
114
+ context 'predicates' do
115
+
116
+ specify '#value? returns true when :fulfilled' do
117
+ expect(pending_subject).to_not be_value
118
+ expect(fulfilled_subject).to be_value
119
+ expect(rejected_subject).to_not be_value
120
+ end
121
+
122
+ specify '#reason? returns true when :rejected' do
123
+ expect(pending_subject).to_not be_reason
124
+ expect(fulfilled_subject).to_not be_reason
125
+ expect(rejected_subject).to be_reason
126
+ end
127
+
128
+ specify '#fulfilled? returns true when :fulfilled' do
129
+ expect(pending_subject).to_not be_fulfilled
130
+ expect(fulfilled_subject).to be_fulfilled
131
+ expect(rejected_subject).to_not be_fulfilled
132
+ end
133
+
134
+ specify '#rejected? returns true when :rejected' do
135
+ expect(pending_subject).to_not be_rejected
136
+ expect(fulfilled_subject).to_not be_rejected
137
+ expect(rejected_subject).to be_rejected
138
+ end
139
+
140
+ specify '#pending? returns true when :pending' do
141
+ expect(pending_subject).to be_pending
142
+ expect(fulfilled_subject).to_not be_pending
143
+ expect(rejected_subject).to_not be_pending
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,237 @@
1
+ require 'spec_helper'
2
+ require_relative 'abstract_struct_shared'
3
+
4
+ module Functional
5
+
6
+ describe Either do
7
+
8
+ let!(:value){ 42 }
9
+ let!(:reason){ StandardError.new }
10
+
11
+ let!(:expected_fields){ [:left, :right] }
12
+ let!(:expected_values){ [value, nil] }
13
+
14
+ let(:struct_class) { Either }
15
+ let(:struct_object) { Either.left(value) }
16
+ let(:other_object) { Either.left(Object.new) }
17
+
18
+ let(:left_subject){ Either.left(reason) }
19
+ let(:right_subject){ Either.right(value) }
20
+
21
+ it_should_behave_like :abstract_struct
22
+
23
+ specify{ Functional::Protocol::Satisfy! Either, :Either }
24
+ specify{ Functional::Protocol::Satisfy! Either, :Disposition }
25
+
26
+ context 'initialization' do
27
+
28
+ it 'cannot be constructed directly' do
29
+ expect {
30
+ Either.new
31
+ }.to raise_error(NameError)
32
+ end
33
+
34
+ it 'sets the left value when constructed by #left' do
35
+ expect(Either.left(value).left).to eq value
36
+ end
37
+
38
+ it 'sets the right value when constructed by #right' do
39
+ expect(Either.right(value).right).to eq value
40
+ end
41
+
42
+ it 'freezes the new object' do
43
+ expect(Either.left(:foo)).to be_frozen
44
+ expect(Either.right(:foo)).to be_frozen
45
+ end
46
+
47
+ it 'aliases #left to #reason' do
48
+ expect(Either.reason(value).left).to eq value
49
+ end
50
+
51
+ it 'aliases #right to #value' do
52
+ expect(Either.value(value).right).to eq value
53
+ end
54
+
55
+ context '#error' do
56
+
57
+ it 'sets left to a StandardError with backtrace when no arguments given' do
58
+ either = Either.error
59
+ expect(either.left).to be_a StandardError
60
+ expect(either.left.message).to_not be nil
61
+ expect(either.left.backtrace).to_not be_empty
62
+ end
63
+
64
+ it 'sets left to a StandardError with the given message' do
65
+ message = 'custom error message'
66
+ either = Either.error(message)
67
+ expect(either.left).to be_a StandardError
68
+ expect(either.left.message).to eq message
69
+ expect(either.left.backtrace).to_not be_empty
70
+ end
71
+
72
+ it 'sets left to an object of the given class with the given message' do
73
+ message = 'custom error message'
74
+ error_class = ArgumentError
75
+ either = Either.error(message, error_class)
76
+ expect(either.left).to be_a error_class
77
+ expect(either.left.message).to eq message
78
+ expect(either.left.backtrace).to_not be_empty
79
+ end
80
+ end
81
+ end
82
+
83
+ context 'state' do
84
+
85
+ specify '#left? returns true when the left value is set' do
86
+ expect(left_subject).to be_left
87
+ end
88
+
89
+ specify '#left? returns false when the right value is set' do
90
+ expect(right_subject).to_not be_left
91
+ end
92
+
93
+ specify '#right? returns true when the right value is set' do
94
+ expect(right_subject).to be_right
95
+ end
96
+
97
+ specify '#right? returns false when the left value is set' do
98
+ expect(left_subject).to_not be_right
99
+ end
100
+
101
+ specify '#left returns the left value when left is set' do
102
+ expect(left_subject.left).to eq reason
103
+ end
104
+
105
+ specify '#left returns nil when right is set' do
106
+ expect(right_subject.left).to be_nil
107
+ end
108
+
109
+ specify '#right returns the right value when right is set' do
110
+ expect(right_subject.right).to eq value
111
+ end
112
+
113
+ specify '#right returns nil when left is set' do
114
+ expect(left_subject.right).to be_nil
115
+ end
116
+
117
+ specify 'aliases #left? as #reason?' do
118
+ expect(left_subject.reason?).to be true
119
+ end
120
+
121
+ specify 'aliases #right? as #value?' do
122
+ expect(right_subject.value?).to be true
123
+ end
124
+
125
+ specify 'aliases #left as #reason' do
126
+ expect(left_subject.reason).to eq reason
127
+ expect(right_subject.reason).to be_nil
128
+ end
129
+
130
+ specify 'aliases #right as #value' do
131
+ expect(right_subject.value).to eq value
132
+ expect(left_subject.value).to be_nil
133
+ end
134
+ end
135
+
136
+ context '#swap' do
137
+
138
+ it 'converts a left projection into a right projection' do
139
+ subject = Either.left(:foo)
140
+ swapped = subject.swap
141
+ expect(swapped).to be_right
142
+ expect(swapped.left).to be_nil
143
+ expect(swapped.right).to eq :foo
144
+ end
145
+
146
+ it 'converts a right projection into a left projection' do
147
+ subject = Either.right(:foo)
148
+ swapped = subject.swap
149
+ expect(swapped).to be_left
150
+ expect(swapped.right).to be_nil
151
+ expect(swapped.left).to eq :foo
152
+ end
153
+ end
154
+
155
+ context '#either' do
156
+
157
+ it 'passes the left value to the left proc when left' do
158
+ expected = nil
159
+ subject = Either.left(100)
160
+ subject.either(
161
+ ->(left) { expected = left },
162
+ ->(right) { expected = -1 }
163
+ )
164
+ expect(expected).to eq 100
165
+ end
166
+
167
+ it 'returns the value of the left proc when left' do
168
+ subject = Either.left(100)
169
+ expect(
170
+ subject.either(
171
+ ->(left) { left * 2 },
172
+ ->(right) { nil }
173
+ )
174
+ ).to eq 200
175
+ end
176
+
177
+ it 'passes the right value to the right proc when right' do
178
+ expected = nil
179
+ subject = Either.right(100)
180
+ subject.either(
181
+ ->(right) { expected = -1 },
182
+ ->(right) { expected = right }
183
+ )
184
+ expect(expected).to eq 100
185
+ end
186
+
187
+ it 'returns the value of the right proc when right' do
188
+ subject = Either.right(100)
189
+ expect(
190
+ subject.either(
191
+ ->(right) { nil },
192
+ ->(right) { right * 2 }
193
+ )
194
+ ).to eq 200
195
+ end
196
+ end
197
+
198
+ context '#iff' do
199
+
200
+ it 'returns a lefty with the given left value when the boolean is true' do
201
+ subject = Either.iff(:foo, :bar, true)
202
+ expect(subject).to be_left
203
+ expect(subject.left).to eq :foo
204
+ end
205
+
206
+ it 'returns a righty with the given right value when the boolean is false' do
207
+ subject = Either.iff(:foo, :bar, false)
208
+ expect(subject).to be_right
209
+ expect(subject.right).to eq :bar
210
+ end
211
+
212
+ it 'returns a lefty with the given left value when the block is truthy' do
213
+ subject = Either.iff(:foo, :bar){ :baz }
214
+ expect(subject).to be_left
215
+ expect(subject.left).to eq :foo
216
+ end
217
+
218
+ it 'returns a righty with the given right value when the block is false' do
219
+ subject = Either.iff(:foo, :bar){ false }
220
+ expect(subject).to be_right
221
+ expect(subject.right).to eq :bar
222
+ end
223
+
224
+ it 'returns a righty with the given right value when the block is nil' do
225
+ subject = Either.iff(:foo, :bar){ nil }
226
+ expect(subject).to be_right
227
+ expect(subject.right).to eq :bar
228
+ end
229
+
230
+ it 'raises an exception when both a boolean and a block are given' do
231
+ expect {
232
+ subject = Either.iff(:foo, :bar, true){ nil }
233
+ }.to raise_error(ArgumentError)
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,207 @@
1
+ require 'spec_helper'
2
+
3
+ module Functional
4
+
5
+ describe Memo do
6
+
7
+ def create_new_memo_class
8
+ Class.new do
9
+ include Functional::Memo
10
+
11
+ class << self
12
+ attr_accessor :count
13
+ end
14
+
15
+ self.count = 0
16
+
17
+ def self.add(a, b)
18
+ self.count += 1
19
+ a + b
20
+ end
21
+ memoize :add
22
+
23
+ def self.increment(n)
24
+ self.count += 1
25
+ end
26
+
27
+ def self.exception(ex = StandardError)
28
+ raise ex
29
+ end
30
+ end
31
+ end
32
+
33
+ subject{ create_new_memo_class }
34
+
35
+ context 'specification' do
36
+
37
+ it 'raises an exception when the method is not defined' do
38
+ expect {
39
+ subject.memoize(:bogus)
40
+ }.to raise_error(NameError)
41
+ end
42
+
43
+ it 'raises an exception when the given method has already been memoized' do
44
+ expect{
45
+ subject.memoize(:add)
46
+ }.to raise_error(ArgumentError)
47
+ end
48
+
49
+ it 'allocates a different cache for each class/module' do
50
+ class_1 = create_new_memo_class
51
+ class_2 = create_new_memo_class
52
+
53
+ 10.times do
54
+ class_1.add(0, 0)
55
+ class_2.add(0, 0)
56
+ end
57
+
58
+ expect(class_1.count).to eq 1
59
+ expect(class_2.count).to eq 1
60
+ end
61
+
62
+ it 'works when included in a class' do
63
+ subject = Class.new do
64
+ include Functional::Memo
65
+ class << self
66
+ attr_accessor :count
67
+ end
68
+ self.count = 0
69
+ def self.foo
70
+ self.count += 1
71
+ end
72
+ memoize :foo
73
+ end
74
+
75
+ 10.times{ subject.foo }
76
+ expect(subject.count).to eq 1
77
+ end
78
+
79
+ it 'works when included in a module' do
80
+ subject = Module.new do
81
+ include Functional::Memo
82
+ class << self
83
+ attr_accessor :count
84
+ end
85
+ self.count = 0
86
+ def self.foo
87
+ self.count += 1
88
+ end
89
+ memoize :foo
90
+ end
91
+
92
+ 10.times{ subject.foo }
93
+ expect(subject.count).to eq 1
94
+ end
95
+
96
+ it 'works when extended by a module' do
97
+ subject = Module.new do
98
+ extend Functional::Memo
99
+ class << self
100
+ attr_accessor :count
101
+ end
102
+ self.count = 0
103
+ def self.foo
104
+ self.count += 1
105
+ end
106
+ memoize :foo
107
+ end
108
+
109
+ 10.times{ subject.foo }
110
+ expect(subject.count).to eq 1
111
+ end
112
+ end
113
+
114
+ context 'caching behavior' do
115
+
116
+ it 'calls the real method on first instance of given args' do
117
+ subject.add(1, 2)
118
+ expect(subject.count).to eq 1
119
+ end
120
+
121
+ it 'calls the real method on first instance of given args' do
122
+ subject.add(1, 2)
123
+ expect(subject.count).to eq 1
124
+ end
125
+
126
+ it 'uses the memo on second instance of given args' do
127
+ 5.times { subject.add(1, 2) }
128
+ expect(subject.count).to eq 1
129
+ end
130
+
131
+ it 'calls the real method when given a block' do
132
+ 5.times { subject.add(1, 2){ nil } }
133
+ expect(subject.count).to eq 5
134
+ end
135
+
136
+ it 'raises an exception when arity does not match' do
137
+ expect {
138
+ subject.add
139
+ }.to raise_error(ArgumentError)
140
+ end
141
+ end
142
+
143
+ context 'maximum cache size' do
144
+
145
+ it 'raises an exception when given a non-positive :at_most' do
146
+ expect {
147
+ subject.memoize(:increment, at_most: -1)
148
+ }.to raise_error(ArgumentError)
149
+ end
150
+
151
+ it 'sets no limit when :at_most not given' do
152
+ subject.memoize(:increment)
153
+ 10000.times{|i| subject.increment(i) }
154
+ expect(subject.count).to eq 10000
155
+ end
156
+
157
+ it 'calls the real method when the :at_most size is reached' do
158
+ subject.memoize(:increment, at_most: 5)
159
+ 10000.times{|i| subject.increment(i % 10) }
160
+ expect(subject.count).to eq 5005
161
+ end
162
+ end
163
+
164
+ context 'thread safety' do
165
+
166
+ let(:mutex){ Mutex.new }
167
+
168
+ before(:each) do
169
+ allow(Mutex).to receive(:new).with(no_args).and_return(mutex)
170
+ allow(mutex).to receive(:lock).with(no_args).and_return(mutex)
171
+ allow(mutex).to receive(:unlock).with(no_args).and_return(mutex)
172
+ end
173
+
174
+ it 'locks a mutex whenever a memoized function is called' do
175
+ expect(mutex).to receive(:lock).exactly(:once).with(no_args)
176
+
177
+ subject.memoize(:increment)
178
+ subject.increment(0)
179
+ end
180
+
181
+ it 'unlocks the mutex whenever a memoized function is called' do
182
+ expect(mutex).to receive(:unlock).exactly(:once).with(no_args)
183
+
184
+ subject.memoize(:increment)
185
+ subject.increment(0)
186
+ end
187
+
188
+ it 'unlocks the mutex when the method call raises an exception' do
189
+ expect(mutex).to receive(:unlock).exactly(:once).with(no_args)
190
+
191
+ subject.memoize(:exception)
192
+ begin
193
+ subject.exception
194
+ rescue
195
+ # suppress
196
+ end
197
+ end
198
+
199
+ it 'uses different mutexes for different functions' do
200
+ expect(Mutex).to receive(:new).with(no_args).exactly(3).times.and_return(mutex)
201
+ # once for memoize(:add) in the definition
202
+ subject.memoize(:increment)
203
+ subject.memoize(:exception)
204
+ end
205
+ end
206
+ end
207
+ end