concurrent-ruby 1.1.5

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 (143) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +478 -0
  3. data/Gemfile +41 -0
  4. data/LICENSE.md +23 -0
  5. data/README.md +381 -0
  6. data/Rakefile +327 -0
  7. data/ext/concurrent-ruby/ConcurrentRubyService.java +17 -0
  8. data/ext/concurrent-ruby/com/concurrent_ruby/ext/AtomicReferenceLibrary.java +175 -0
  9. data/ext/concurrent-ruby/com/concurrent_ruby/ext/JRubyMapBackendLibrary.java +248 -0
  10. data/ext/concurrent-ruby/com/concurrent_ruby/ext/JavaAtomicBooleanLibrary.java +93 -0
  11. data/ext/concurrent-ruby/com/concurrent_ruby/ext/JavaAtomicFixnumLibrary.java +113 -0
  12. data/ext/concurrent-ruby/com/concurrent_ruby/ext/JavaSemaphoreLibrary.java +159 -0
  13. data/ext/concurrent-ruby/com/concurrent_ruby/ext/SynchronizationLibrary.java +307 -0
  14. data/ext/concurrent-ruby/com/concurrent_ruby/ext/jsr166e/ConcurrentHashMap.java +31 -0
  15. data/ext/concurrent-ruby/com/concurrent_ruby/ext/jsr166e/ConcurrentHashMapV8.java +3863 -0
  16. data/ext/concurrent-ruby/com/concurrent_ruby/ext/jsr166e/LongAdder.java +203 -0
  17. data/ext/concurrent-ruby/com/concurrent_ruby/ext/jsr166e/Striped64.java +342 -0
  18. data/ext/concurrent-ruby/com/concurrent_ruby/ext/jsr166e/nounsafe/ConcurrentHashMapV8.java +3800 -0
  19. data/ext/concurrent-ruby/com/concurrent_ruby/ext/jsr166e/nounsafe/LongAdder.java +204 -0
  20. data/ext/concurrent-ruby/com/concurrent_ruby/ext/jsr166e/nounsafe/Striped64.java +291 -0
  21. data/ext/concurrent-ruby/com/concurrent_ruby/ext/jsr166y/ThreadLocalRandom.java +199 -0
  22. data/lib/concurrent-ruby.rb +1 -0
  23. data/lib/concurrent.rb +134 -0
  24. data/lib/concurrent/agent.rb +587 -0
  25. data/lib/concurrent/array.rb +66 -0
  26. data/lib/concurrent/async.rb +459 -0
  27. data/lib/concurrent/atom.rb +222 -0
  28. data/lib/concurrent/atomic/abstract_thread_local_var.rb +66 -0
  29. data/lib/concurrent/atomic/atomic_boolean.rb +126 -0
  30. data/lib/concurrent/atomic/atomic_fixnum.rb +143 -0
  31. data/lib/concurrent/atomic/atomic_markable_reference.rb +164 -0
  32. data/lib/concurrent/atomic/atomic_reference.rb +204 -0
  33. data/lib/concurrent/atomic/count_down_latch.rb +100 -0
  34. data/lib/concurrent/atomic/cyclic_barrier.rb +128 -0
  35. data/lib/concurrent/atomic/event.rb +109 -0
  36. data/lib/concurrent/atomic/java_count_down_latch.rb +42 -0
  37. data/lib/concurrent/atomic/java_thread_local_var.rb +37 -0
  38. data/lib/concurrent/atomic/mutex_atomic_boolean.rb +62 -0
  39. data/lib/concurrent/atomic/mutex_atomic_fixnum.rb +75 -0
  40. data/lib/concurrent/atomic/mutex_count_down_latch.rb +44 -0
  41. data/lib/concurrent/atomic/mutex_semaphore.rb +115 -0
  42. data/lib/concurrent/atomic/read_write_lock.rb +254 -0
  43. data/lib/concurrent/atomic/reentrant_read_write_lock.rb +379 -0
  44. data/lib/concurrent/atomic/ruby_thread_local_var.rb +161 -0
  45. data/lib/concurrent/atomic/semaphore.rb +145 -0
  46. data/lib/concurrent/atomic/thread_local_var.rb +104 -0
  47. data/lib/concurrent/atomic_reference/mutex_atomic.rb +56 -0
  48. data/lib/concurrent/atomic_reference/numeric_cas_wrapper.rb +28 -0
  49. data/lib/concurrent/atomics.rb +10 -0
  50. data/lib/concurrent/collection/copy_on_notify_observer_set.rb +107 -0
  51. data/lib/concurrent/collection/copy_on_write_observer_set.rb +111 -0
  52. data/lib/concurrent/collection/java_non_concurrent_priority_queue.rb +84 -0
  53. data/lib/concurrent/collection/lock_free_stack.rb +158 -0
  54. data/lib/concurrent/collection/map/atomic_reference_map_backend.rb +927 -0
  55. data/lib/concurrent/collection/map/mri_map_backend.rb +66 -0
  56. data/lib/concurrent/collection/map/non_concurrent_map_backend.rb +140 -0
  57. data/lib/concurrent/collection/map/synchronized_map_backend.rb +82 -0
  58. data/lib/concurrent/collection/non_concurrent_priority_queue.rb +143 -0
  59. data/lib/concurrent/collection/ruby_non_concurrent_priority_queue.rb +150 -0
  60. data/lib/concurrent/concern/deprecation.rb +34 -0
  61. data/lib/concurrent/concern/dereferenceable.rb +73 -0
  62. data/lib/concurrent/concern/logging.rb +32 -0
  63. data/lib/concurrent/concern/obligation.rb +220 -0
  64. data/lib/concurrent/concern/observable.rb +110 -0
  65. data/lib/concurrent/concurrent_ruby.jar +0 -0
  66. data/lib/concurrent/configuration.rb +184 -0
  67. data/lib/concurrent/constants.rb +8 -0
  68. data/lib/concurrent/dataflow.rb +81 -0
  69. data/lib/concurrent/delay.rb +199 -0
  70. data/lib/concurrent/errors.rb +69 -0
  71. data/lib/concurrent/exchanger.rb +352 -0
  72. data/lib/concurrent/executor/abstract_executor_service.rb +134 -0
  73. data/lib/concurrent/executor/cached_thread_pool.rb +62 -0
  74. data/lib/concurrent/executor/executor_service.rb +185 -0
  75. data/lib/concurrent/executor/fixed_thread_pool.rb +206 -0
  76. data/lib/concurrent/executor/immediate_executor.rb +66 -0
  77. data/lib/concurrent/executor/indirect_immediate_executor.rb +44 -0
  78. data/lib/concurrent/executor/java_executor_service.rb +91 -0
  79. data/lib/concurrent/executor/java_single_thread_executor.rb +29 -0
  80. data/lib/concurrent/executor/java_thread_pool_executor.rb +123 -0
  81. data/lib/concurrent/executor/ruby_executor_service.rb +78 -0
  82. data/lib/concurrent/executor/ruby_single_thread_executor.rb +22 -0
  83. data/lib/concurrent/executor/ruby_thread_pool_executor.rb +362 -0
  84. data/lib/concurrent/executor/safe_task_executor.rb +35 -0
  85. data/lib/concurrent/executor/serial_executor_service.rb +34 -0
  86. data/lib/concurrent/executor/serialized_execution.rb +107 -0
  87. data/lib/concurrent/executor/serialized_execution_delegator.rb +28 -0
  88. data/lib/concurrent/executor/simple_executor_service.rb +100 -0
  89. data/lib/concurrent/executor/single_thread_executor.rb +56 -0
  90. data/lib/concurrent/executor/thread_pool_executor.rb +87 -0
  91. data/lib/concurrent/executor/timer_set.rb +173 -0
  92. data/lib/concurrent/executors.rb +20 -0
  93. data/lib/concurrent/future.rb +141 -0
  94. data/lib/concurrent/hash.rb +59 -0
  95. data/lib/concurrent/immutable_struct.rb +93 -0
  96. data/lib/concurrent/ivar.rb +207 -0
  97. data/lib/concurrent/map.rb +337 -0
  98. data/lib/concurrent/maybe.rb +229 -0
  99. data/lib/concurrent/mutable_struct.rb +229 -0
  100. data/lib/concurrent/mvar.rb +242 -0
  101. data/lib/concurrent/options.rb +42 -0
  102. data/lib/concurrent/promise.rb +579 -0
  103. data/lib/concurrent/promises.rb +2167 -0
  104. data/lib/concurrent/re_include.rb +58 -0
  105. data/lib/concurrent/scheduled_task.rb +318 -0
  106. data/lib/concurrent/set.rb +66 -0
  107. data/lib/concurrent/settable_struct.rb +129 -0
  108. data/lib/concurrent/synchronization.rb +30 -0
  109. data/lib/concurrent/synchronization/abstract_lockable_object.rb +98 -0
  110. data/lib/concurrent/synchronization/abstract_object.rb +24 -0
  111. data/lib/concurrent/synchronization/abstract_struct.rb +160 -0
  112. data/lib/concurrent/synchronization/condition.rb +60 -0
  113. data/lib/concurrent/synchronization/jruby_lockable_object.rb +13 -0
  114. data/lib/concurrent/synchronization/jruby_object.rb +45 -0
  115. data/lib/concurrent/synchronization/lock.rb +36 -0
  116. data/lib/concurrent/synchronization/lockable_object.rb +74 -0
  117. data/lib/concurrent/synchronization/mri_object.rb +44 -0
  118. data/lib/concurrent/synchronization/mutex_lockable_object.rb +76 -0
  119. data/lib/concurrent/synchronization/object.rb +183 -0
  120. data/lib/concurrent/synchronization/rbx_lockable_object.rb +65 -0
  121. data/lib/concurrent/synchronization/rbx_object.rb +49 -0
  122. data/lib/concurrent/synchronization/truffleruby_object.rb +47 -0
  123. data/lib/concurrent/synchronization/volatile.rb +36 -0
  124. data/lib/concurrent/thread_safe/synchronized_delegator.rb +50 -0
  125. data/lib/concurrent/thread_safe/util.rb +16 -0
  126. data/lib/concurrent/thread_safe/util/adder.rb +74 -0
  127. data/lib/concurrent/thread_safe/util/cheap_lockable.rb +118 -0
  128. data/lib/concurrent/thread_safe/util/data_structures.rb +63 -0
  129. data/lib/concurrent/thread_safe/util/power_of_two_tuple.rb +38 -0
  130. data/lib/concurrent/thread_safe/util/striped64.rb +246 -0
  131. data/lib/concurrent/thread_safe/util/volatile.rb +75 -0
  132. data/lib/concurrent/thread_safe/util/xor_shift_random.rb +50 -0
  133. data/lib/concurrent/timer_task.rb +334 -0
  134. data/lib/concurrent/tuple.rb +86 -0
  135. data/lib/concurrent/tvar.rb +258 -0
  136. data/lib/concurrent/utility/at_exit.rb +97 -0
  137. data/lib/concurrent/utility/engine.rb +56 -0
  138. data/lib/concurrent/utility/monotonic_time.rb +58 -0
  139. data/lib/concurrent/utility/native_extension_loader.rb +79 -0
  140. data/lib/concurrent/utility/native_integer.rb +53 -0
  141. data/lib/concurrent/utility/processor_counter.rb +158 -0
  142. data/lib/concurrent/version.rb +3 -0
  143. metadata +193 -0
@@ -0,0 +1,337 @@
1
+ require 'thread'
2
+ require 'concurrent/constants'
3
+ require 'concurrent/synchronization'
4
+ require 'concurrent/utility/engine'
5
+
6
+ module Concurrent
7
+ # @!visibility private
8
+ module Collection
9
+
10
+ # @!visibility private
11
+ MapImplementation = case
12
+ when Concurrent.on_jruby?
13
+ # noinspection RubyResolve
14
+ JRubyMapBackend
15
+ when Concurrent.on_cruby?
16
+ require 'concurrent/collection/map/mri_map_backend'
17
+ MriMapBackend
18
+ when Concurrent.on_rbx? || Concurrent.on_truffleruby?
19
+ require 'concurrent/collection/map/atomic_reference_map_backend'
20
+ AtomicReferenceMapBackend
21
+ else
22
+ warn 'Concurrent::Map: unsupported Ruby engine, using a fully synchronized Concurrent::Map implementation'
23
+ require 'concurrent/collection/map/synchronized_map_backend'
24
+ SynchronizedMapBackend
25
+ end
26
+ end
27
+
28
+ # `Concurrent::Map` is a hash-like object and should have much better performance
29
+ # characteristics, especially under high concurrency, than `Concurrent::Hash`.
30
+ # However, `Concurrent::Map `is not strictly semantically equivalent to a ruby `Hash`
31
+ # -- for instance, it does not necessarily retain ordering by insertion time as `Hash`
32
+ # does. For most uses it should do fine though, and we recommend you consider
33
+ # `Concurrent::Map` instead of `Concurrent::Hash` for your concurrency-safe hash needs.
34
+ class Map < Collection::MapImplementation
35
+
36
+ # @!macro map.atomic_method
37
+ # This method is atomic.
38
+
39
+ # @!macro map.atomic_method_with_block
40
+ # This method is atomic.
41
+ # @note Atomic methods taking a block do not allow the `self` instance
42
+ # to be used within the block. Doing so will cause a deadlock.
43
+
44
+ # @!method compute_if_absent(key)
45
+ # Compute and store new value for key if the key is absent.
46
+ # @param [Object] key
47
+ # @yield new value
48
+ # @yieldreturn [Object] new value
49
+ # @return [Object] new value or current value
50
+ # @!macro map.atomic_method_with_block
51
+
52
+ # @!method compute_if_present(key)
53
+ # Compute and store new value for key if the key is present.
54
+ # @param [Object] key
55
+ # @yield new value
56
+ # @yieldparam old_value [Object]
57
+ # @yieldreturn [Object, nil] new value, when nil the key is removed
58
+ # @return [Object, nil] new value or nil
59
+ # @!macro map.atomic_method_with_block
60
+
61
+ # @!method compute(key)
62
+ # Compute and store new value for key.
63
+ # @param [Object] key
64
+ # @yield compute new value from old one
65
+ # @yieldparam old_value [Object, nil] old_value, or nil when key is absent
66
+ # @yieldreturn [Object, nil] new value, when nil the key is removed
67
+ # @return [Object, nil] new value or nil
68
+ # @!macro map.atomic_method_with_block
69
+
70
+ # @!method merge_pair(key, value)
71
+ # If the key is absent, the value is stored, otherwise new value is
72
+ # computed with a block.
73
+ # @param [Object] key
74
+ # @param [Object] value
75
+ # @yield compute new value from old one
76
+ # @yieldparam old_value [Object] old value
77
+ # @yieldreturn [Object, nil] new value, when nil the key is removed
78
+ # @return [Object, nil] new value or nil
79
+ # @!macro map.atomic_method_with_block
80
+
81
+ # @!method replace_pair(key, old_value, new_value)
82
+ # Replaces old_value with new_value if key exists and current value
83
+ # matches old_value
84
+ # @param [Object] key
85
+ # @param [Object] old_value
86
+ # @param [Object] new_value
87
+ # @return [true, false] true if replaced
88
+ # @!macro map.atomic_method
89
+
90
+ # @!method replace_if_exists(key, new_value)
91
+ # Replaces current value with new_value if key exists
92
+ # @param [Object] key
93
+ # @param [Object] new_value
94
+ # @return [Object, nil] old value or nil
95
+ # @!macro map.atomic_method
96
+
97
+ # @!method get_and_set(key, value)
98
+ # Get the current value under key and set new value.
99
+ # @param [Object] key
100
+ # @param [Object] value
101
+ # @return [Object, nil] old value or nil when the key was absent
102
+ # @!macro map.atomic_method
103
+
104
+ # @!method delete(key)
105
+ # Delete key and its value.
106
+ # @param [Object] key
107
+ # @return [Object, nil] old value or nil when the key was absent
108
+ # @!macro map.atomic_method
109
+
110
+ # @!method delete_pair(key, value)
111
+ # Delete pair and its value if current value equals the provided value.
112
+ # @param [Object] key
113
+ # @param [Object] value
114
+ # @return [true, false] true if deleted
115
+ # @!macro map.atomic_method
116
+
117
+
118
+ def initialize(options = nil, &block)
119
+ if options.kind_of?(::Hash)
120
+ validate_options_hash!(options)
121
+ else
122
+ options = nil
123
+ end
124
+
125
+ super(options)
126
+ @default_proc = block
127
+ end
128
+
129
+ # Get a value with key
130
+ # @param [Object] key
131
+ # @return [Object] the value
132
+ def [](key)
133
+ if value = super # non-falsy value is an existing mapping, return it right away
134
+ value
135
+ # re-check is done with get_or_default(key, NULL) instead of a simple !key?(key) in order to avoid a race condition, whereby by the time the current thread gets to the key?(key) call
136
+ # a key => value mapping might have already been created by a different thread (key?(key) would then return true, this elsif branch wouldn't be taken and an incorrent +nil+ value
137
+ # would be returned)
138
+ # note: nil == value check is not technically necessary
139
+ elsif @default_proc && nil == value && NULL == (value = get_or_default(key, NULL))
140
+ @default_proc.call(self, key)
141
+ else
142
+ value
143
+ end
144
+ end
145
+
146
+ alias_method :get, :[]
147
+ # TODO (pitr-ch 30-Oct-2018): doc
148
+ alias_method :put, :[]=
149
+
150
+ # Get a value with key, or default_value when key is absent,
151
+ # or fail when no default_value is given.
152
+ # @param [Object] key
153
+ # @param [Object] default_value
154
+ # @yield default value for a key
155
+ # @yieldparam key [Object]
156
+ # @yieldreturn [Object] default value
157
+ # @return [Object] the value or default value
158
+ # @raise [KeyError] when key is missing and no default_value is provided
159
+ # @!macro map_method_not_atomic
160
+ # @note The "fetch-then-act" methods of `Map` are not atomic. `Map` is intended
161
+ # to be use as a concurrency primitive with strong happens-before
162
+ # guarantees. It is not intended to be used as a high-level abstraction
163
+ # supporting complex operations. All read and write operations are
164
+ # thread safe, but no guarantees are made regarding race conditions
165
+ # between the fetch operation and yielding to the block. Additionally,
166
+ # this method does not support recursion. This is due to internal
167
+ # constraints that are very unlikely to change in the near future.
168
+ def fetch(key, default_value = NULL)
169
+ if NULL != (value = get_or_default(key, NULL))
170
+ value
171
+ elsif block_given?
172
+ yield key
173
+ elsif NULL != default_value
174
+ default_value
175
+ else
176
+ raise_fetch_no_key
177
+ end
178
+ end
179
+
180
+ # Fetch value with key, or store default value when key is absent,
181
+ # or fail when no default_value is given. This is a two step operation,
182
+ # therefore not atomic. The store can overwrite other concurrently
183
+ # stored value.
184
+ # @param [Object] key
185
+ # @param [Object] default_value
186
+ # @yield default value for a key
187
+ # @yieldparam key [Object]
188
+ # @yieldreturn [Object] default value
189
+ # @return [Object] the value or default value
190
+ # @!macro map.atomic_method_with_block
191
+ def fetch_or_store(key, default_value = NULL)
192
+ fetch(key) do
193
+ put(key, block_given? ? yield(key) : (NULL == default_value ? raise_fetch_no_key : default_value))
194
+ end
195
+ end
196
+
197
+ # Insert value into map with key if key is absent in one atomic step.
198
+ # @param [Object] key
199
+ # @param [Object] value
200
+ # @return [Object, nil] the value or nil when key was present
201
+ def put_if_absent(key, value)
202
+ computed = false
203
+ result = compute_if_absent(key) do
204
+ computed = true
205
+ value
206
+ end
207
+ computed ? nil : result
208
+ end unless method_defined?(:put_if_absent)
209
+
210
+ # Is the value stored in the map. Iterates over all values.
211
+ # @param [Object] value
212
+ # @return [true, false]
213
+ def value?(value)
214
+ each_value do |v|
215
+ return true if value.equal?(v)
216
+ end
217
+ false
218
+ end
219
+
220
+ # All keys
221
+ # @return [::Array<Object>] keys
222
+ def keys
223
+ arr = []
224
+ each_pair { |k, v| arr << k }
225
+ arr
226
+ end unless method_defined?(:keys)
227
+
228
+ # All values
229
+ # @return [::Array<Object>] values
230
+ def values
231
+ arr = []
232
+ each_pair { |k, v| arr << v }
233
+ arr
234
+ end unless method_defined?(:values)
235
+
236
+ # Iterates over each key.
237
+ # @yield for each key in the map
238
+ # @yieldparam key [Object]
239
+ # @return [self]
240
+ # @!macro map.atomic_method_with_block
241
+ def each_key
242
+ each_pair { |k, v| yield k }
243
+ end unless method_defined?(:each_key)
244
+
245
+ # Iterates over each value.
246
+ # @yield for each value in the map
247
+ # @yieldparam value [Object]
248
+ # @return [self]
249
+ # @!macro map.atomic_method_with_block
250
+ def each_value
251
+ each_pair { |k, v| yield v }
252
+ end unless method_defined?(:each_value)
253
+
254
+ # Iterates over each key value pair.
255
+ # @yield for each key value pair in the map
256
+ # @yieldparam key [Object]
257
+ # @yieldparam value [Object]
258
+ # @return [self]
259
+ # @!macro map.atomic_method_with_block
260
+ def each_pair
261
+ return enum_for :each_pair unless block_given?
262
+ super
263
+ end
264
+
265
+ alias_method :each, :each_pair unless method_defined?(:each)
266
+
267
+ # Find key of a value.
268
+ # @param [Object] value
269
+ # @return [Object, nil] key or nil when not found
270
+ def key(value)
271
+ each_pair { |k, v| return k if v == value }
272
+ nil
273
+ end unless method_defined?(:key)
274
+ alias_method :index, :key if RUBY_VERSION < '1.9'
275
+
276
+ # Is map empty?
277
+ # @return [true, false]
278
+ def empty?
279
+ each_pair { |k, v| return false }
280
+ true
281
+ end unless method_defined?(:empty?)
282
+
283
+ # The size of map.
284
+ # @return [Integer] size
285
+ def size
286
+ count = 0
287
+ each_pair { |k, v| count += 1 }
288
+ count
289
+ end unless method_defined?(:size)
290
+
291
+ # @!visibility private
292
+ def marshal_dump
293
+ raise TypeError, "can't dump hash with default proc" if @default_proc
294
+ h = {}
295
+ each_pair { |k, v| h[k] = v }
296
+ h
297
+ end
298
+
299
+ # @!visibility private
300
+ def marshal_load(hash)
301
+ initialize
302
+ populate_from(hash)
303
+ end
304
+
305
+ undef :freeze
306
+
307
+ # @!visibility private
308
+ def inspect
309
+ format '%s entries=%d default_proc=%s>', to_s[0..-2], size.to_s, @default_proc.inspect
310
+ end
311
+
312
+ private
313
+
314
+ def raise_fetch_no_key
315
+ raise KeyError, 'key not found'
316
+ end
317
+
318
+ def initialize_copy(other)
319
+ super
320
+ populate_from(other)
321
+ end
322
+
323
+ def populate_from(hash)
324
+ hash.each_pair { |k, v| self[k] = v }
325
+ self
326
+ end
327
+
328
+ def validate_options_hash!(options)
329
+ if (initial_capacity = options[:initial_capacity]) && (!initial_capacity.kind_of?(Integer) || initial_capacity < 0)
330
+ raise ArgumentError, ":initial_capacity must be a positive Integer"
331
+ end
332
+ if (load_factor = options[:load_factor]) && (!load_factor.kind_of?(Numeric) || load_factor <= 0 || load_factor > 1)
333
+ raise ArgumentError, ":load_factor must be a number between 0 and 1"
334
+ end
335
+ end
336
+ end
337
+ end
@@ -0,0 +1,229 @@
1
+ require 'concurrent/synchronization'
2
+
3
+ module Concurrent
4
+
5
+ # A `Maybe` encapsulates an optional value. A `Maybe` either contains a value
6
+ # of (represented as `Just`), or it is empty (represented as `Nothing`). Using
7
+ # `Maybe` is a good way to deal with errors or exceptional cases without
8
+ # resorting to drastic measures such as exceptions.
9
+ #
10
+ # `Maybe` is a replacement for the use of `nil` with better type checking.
11
+ #
12
+ # For compatibility with {Concurrent::Concern::Obligation} the predicate and
13
+ # accessor methods are aliased as `fulfilled?`, `rejected?`, `value`, and
14
+ # `reason`.
15
+ #
16
+ # ## Motivation
17
+ #
18
+ # A common pattern in languages with pattern matching, such as Erlang and
19
+ # Haskell, is to return *either* a value *or* an error from a function
20
+ # Consider this Erlang code:
21
+ #
22
+ # ```erlang
23
+ # case file:consult("data.dat") of
24
+ # {ok, Terms} -> do_something_useful(Terms);
25
+ # {error, Reason} -> lager:error(Reason)
26
+ # end.
27
+ # ```
28
+ #
29
+ # In this example the standard library function `file:consult` returns a
30
+ # [tuple](http://erlang.org/doc/reference_manual/data_types.html#id69044)
31
+ # with two elements: an [atom](http://erlang.org/doc/reference_manual/data_types.html#id64134)
32
+ # (similar to a ruby symbol) and a variable containing ancillary data. On
33
+ # success it returns the atom `ok` and the data from the file. On failure it
34
+ # returns `error` and a string with an explanation of the problem. With this
35
+ # pattern there is no ambiguity regarding success or failure. If the file is
36
+ # empty the return value cannot be misinterpreted as an error. And when an
37
+ # error occurs the return value provides useful information.
38
+ #
39
+ # In Ruby we tend to return `nil` when an error occurs or else we raise an
40
+ # exception. Both of these idioms are problematic. Returning `nil` is
41
+ # ambiguous because `nil` may also be a valid value. It also lacks
42
+ # information pertaining to the nature of the error. Raising an exception
43
+ # is both expensive and usurps the normal flow of control. All of these
44
+ # problems can be solved with the use of a `Maybe`.
45
+ #
46
+ # A `Maybe` is unambiguous with regard to whether or not it contains a value.
47
+ # When `Just` it contains a value, when `Nothing` it does not. When `Just`
48
+ # the value it contains may be `nil`, which is perfectly valid. When
49
+ # `Nothing` the reason for the lack of a value is contained as well. The
50
+ # previous Erlang example can be duplicated in Ruby in a principled way by
51
+ # having functions return `Maybe` objects:
52
+ #
53
+ # ```ruby
54
+ # result = MyFileUtils.consult("data.dat") # returns a Maybe
55
+ # if result.just?
56
+ # do_something_useful(result.value) # or result.just
57
+ # else
58
+ # logger.error(result.reason) # or result.nothing
59
+ # end
60
+ # ```
61
+ #
62
+ # @example Returning a Maybe from a Function
63
+ # module MyFileUtils
64
+ # def self.consult(path)
65
+ # file = File.open(path, 'r')
66
+ # Concurrent::Maybe.just(file.read)
67
+ # rescue => ex
68
+ # return Concurrent::Maybe.nothing(ex)
69
+ # ensure
70
+ # file.close if file
71
+ # end
72
+ # end
73
+ #
74
+ # maybe = MyFileUtils.consult('bogus.file')
75
+ # maybe.just? #=> false
76
+ # maybe.nothing? #=> true
77
+ # maybe.reason #=> #<Errno::ENOENT: No such file or directory @ rb_sysopen - bogus.file>
78
+ #
79
+ # maybe = MyFileUtils.consult('README.md')
80
+ # maybe.just? #=> true
81
+ # maybe.nothing? #=> false
82
+ # maybe.value #=> "# Concurrent Ruby\n[![Gem Version..."
83
+ #
84
+ # @example Using Maybe with a Block
85
+ # result = Concurrent::Maybe.from do
86
+ # Client.find(10) # Client is an ActiveRecord model
87
+ # end
88
+ #
89
+ # # -- if the record was found
90
+ # result.just? #=> true
91
+ # result.value #=> #<Client id: 10, first_name: "Ryan">
92
+ #
93
+ # # -- if the record was not found
94
+ # result.just? #=> false
95
+ # result.reason #=> ActiveRecord::RecordNotFound
96
+ #
97
+ # @example Using Maybe with the Null Object Pattern
98
+ # # In a Rails controller...
99
+ # result = ClientService.new(10).find # returns a Maybe
100
+ # render json: result.or(NullClient.new)
101
+ #
102
+ # @see https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Maybe.html Haskell Data.Maybe
103
+ # @see https://github.com/purescript/purescript-maybe/blob/master/docs/Data.Maybe.md PureScript Data.Maybe
104
+ class Maybe < Synchronization::Object
105
+ include Comparable
106
+ safe_initialization!
107
+
108
+ # Indicates that the given attribute has not been set.
109
+ # When `Just` the {#nothing} getter will return `NONE`.
110
+ # When `Nothing` the {#just} getter will return `NONE`.
111
+ NONE = ::Object.new.freeze
112
+
113
+ # The value of a `Maybe` when `Just`. Will be `NONE` when `Nothing`.
114
+ attr_reader :just
115
+
116
+ # The reason for the `Maybe` when `Nothing`. Will be `NONE` when `Just`.
117
+ attr_reader :nothing
118
+
119
+ private_class_method :new
120
+
121
+ # Create a new `Maybe` using the given block.
122
+ #
123
+ # Runs the given block passing all function arguments to the block as block
124
+ # arguments. If the block runs to completion without raising an exception
125
+ # a new `Just` is created with the value set to the return value of the
126
+ # block. If the block raises an exception a new `Nothing` is created with
127
+ # the reason being set to the raised exception.
128
+ #
129
+ # @param [Array<Object>] args Zero or more arguments to pass to the block.
130
+ # @yield The block from which to create a new `Maybe`.
131
+ # @yieldparam [Array<Object>] args Zero or more block arguments passed as
132
+ # arguments to the function.
133
+ #
134
+ # @return [Maybe] The newly created object.
135
+ #
136
+ # @raise [ArgumentError] when no block given.
137
+ def self.from(*args)
138
+ raise ArgumentError.new('no block given') unless block_given?
139
+ begin
140
+ value = yield(*args)
141
+ return new(value, NONE)
142
+ rescue => ex
143
+ return new(NONE, ex)
144
+ end
145
+ end
146
+
147
+ # Create a new `Just` with the given value.
148
+ #
149
+ # @param [Object] value The value to set for the new `Maybe` object.
150
+ #
151
+ # @return [Maybe] The newly created object.
152
+ def self.just(value)
153
+ return new(value, NONE)
154
+ end
155
+
156
+ # Create a new `Nothing` with the given (optional) reason.
157
+ #
158
+ # @param [Exception] error The reason to set for the new `Maybe` object.
159
+ # When given a string a new `StandardError` will be created with the
160
+ # argument as the message. When no argument is given a new
161
+ # `StandardError` with an empty message will be created.
162
+ #
163
+ # @return [Maybe] The newly created object.
164
+ def self.nothing(error = '')
165
+ if error.is_a?(Exception)
166
+ nothing = error
167
+ else
168
+ nothing = StandardError.new(error.to_s)
169
+ end
170
+ return new(NONE, nothing)
171
+ end
172
+
173
+ # Is this `Maybe` a `Just` (successfully fulfilled with a value)?
174
+ #
175
+ # @return [Boolean] True if `Just` or false if `Nothing`.
176
+ def just?
177
+ ! nothing?
178
+ end
179
+ alias :fulfilled? :just?
180
+
181
+ # Is this `Maybe` a `nothing` (rejected with an exception upon fulfillment)?
182
+ #
183
+ # @return [Boolean] True if `Nothing` or false if `Just`.
184
+ def nothing?
185
+ @nothing != NONE
186
+ end
187
+ alias :rejected? :nothing?
188
+
189
+ alias :value :just
190
+
191
+ alias :reason :nothing
192
+
193
+ # Comparison operator.
194
+ #
195
+ # @return [Integer] 0 if self and other are both `Nothing`;
196
+ # -1 if self is `Nothing` and other is `Just`;
197
+ # 1 if self is `Just` and other is nothing;
198
+ # `self.just <=> other.just` if both self and other are `Just`.
199
+ def <=>(other)
200
+ if nothing?
201
+ other.nothing? ? 0 : -1
202
+ else
203
+ other.nothing? ? 1 : just <=> other.just
204
+ end
205
+ end
206
+
207
+ # Return either the value of self or the given default value.
208
+ #
209
+ # @return [Object] The value of self when `Just`; else the given default.
210
+ def or(other)
211
+ just? ? just : other
212
+ end
213
+
214
+ private
215
+
216
+ # Create a new `Maybe` with the given attributes.
217
+ #
218
+ # @param [Object] just The value when `Just` else `NONE`.
219
+ # @param [Exception, Object] nothing The exception when `Nothing` else `NONE`.
220
+ #
221
+ # @return [Maybe] The new `Maybe`.
222
+ #
223
+ # @!visibility private
224
+ def initialize(just, nothing)
225
+ @just = just
226
+ @nothing = nothing
227
+ end
228
+ end
229
+ end