Skip to content

Commit b41c7e0

Browse files
committed
Merge pull request #310 from ruby-concurrency/maybe
Maybe
2 parents ea580e2 + 3af974e commit b41c7e0

File tree

5 files changed

+500
-44
lines changed

5 files changed

+500
-44
lines changed

lib/concurrent.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
require 'concurrent/delay'
2020
require 'concurrent/future'
2121
require 'concurrent/ivar'
22+
require 'concurrent/maybe'
2223
require 'concurrent/mvar'
2324
require 'concurrent/promise'
2425
require 'concurrent/scheduled_task'

lib/concurrent/ivar.rb

Lines changed: 2 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,40 +26,7 @@ module Concurrent
2626
# when the values they depend on are ready you want `dataflow`. `IVar` is
2727
# generally a low-level primitive.
2828
#
29-
# @!macro [attach] copy_options
30-
# ## Copy Options
31-
#
32-
# Object references in Ruby are mutable. This can lead to serious
33-
# problems when the {#value} of an object is a mutable reference. Which
34-
# is always the case unless the value is a `Fixnum`, `Symbol`, or similar
35-
# "primative" data type. Each instance can be configured with a few
36-
# options that can help protect the program from potentially dangerous
37-
# operations. Each of these options can be optionally set when the oject
38-
# instance is created:
39-
#
40-
# * `:dup_on_deref` When true the object will call the `#dup` method on
41-
# the `value` object every time the `#value` methid is called
42-
# (default: false)
43-
# * `:freeze_on_deref` When true the object will call the `#freeze`
44-
# method on the `value` object every time the `#value` method is called
45-
# (default: false)
46-
# * `:copy_on_deref` When given a `Proc` object the `Proc` will be run
47-
# every time the `#value` method is called. The `Proc` will be given
48-
# the current `value` as its only argument and the result returned by
49-
# the block will be the return value of the `#value` call. When `nil`
50-
# this option will be ignored (default: nil)
51-
#
52-
# When multiple deref options are set the order of operations is strictly defined.
53-
# The order of deref operations is:
54-
# * `:copy_on_deref`
55-
# * `:dup_on_deref`
56-
# * `:freeze_on_deref`
57-
#
58-
# Because of this ordering there is no need to `#freeze` an object created by a
59-
# provided `:copy_on_deref` block. Simply set `:freeze_on_deref` to `true`.
60-
# Setting both `:dup_on_deref` to `true` and `:freeze_on_deref` to `true` is
61-
# as close to the behavior of a "pure" functional language (like Erlang, Clojure,
62-
# or Haskell) as we are likely to get in Ruby.
29+
# @!macro copy_options
6330
#
6431
# ## Examples
6532
#
@@ -91,14 +58,7 @@ class IVar < Synchronization::Object
9158
# @param [Object] value the initial value
9259
# @param [Hash] opts the options to create a message with
9360
#
94-
# @!macro [attach] deref_options
95-
# @option opts [Boolean] :dup_on_deref (false) Call `#dup` before
96-
# returning the data from {#value}
97-
# @option opts [Boolean] :freeze_on_deref (false) Call `#freeze` before
98-
# returning the data from {#value}
99-
# @option opts [Proc] :copy_on_deref (nil) When calling the {#value}
100-
# method, call the given proc passing the internal value as the sole
101-
# argument then return the new value returned from the proc.
61+
# @!macro deref_options
10262
def initialize(value = NO_VALUE, opts = {}, &block)
10363
if value != NO_VALUE && block_given?
10464
raise ArgumentError.new('provide only a value or a block')

lib/concurrent/maybe.rb

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
module Concurrent
2+
3+
# A `Maybe` encapsulates an optional value. A `Maybe` either contains a value
4+
# of (represented as `Just`), or it is empty (represented as `Nothing`). Using
5+
# `Maybe` is a good way to deal with errors or exceptional cases without
6+
# resorting to drastic measures such as exceptions.
7+
#
8+
# `Maybe` is a replacement for the use of `nil` with better type checking.
9+
#
10+
# For compatibility with {Concurrent::Obligation} the predicate and
11+
# accessor methods are aliased as `fulfilled?`, `rejected?`, `value`, and
12+
# `reason`.
13+
#
14+
# ## Motivation
15+
#
16+
# A common pattern in languages with pattern matching, such as Erlang and
17+
# Haskell, is to return *either* a value *or* an error from a function
18+
# Consider this Erlang code:
19+
#
20+
# ```erlang
21+
# case file:consult("data.dat") of
22+
# {ok, Terms} -> do_something_useful(Terms);
23+
# {error, Reason} -> lager:error(Reason)
24+
# end.
25+
# ```
26+
#
27+
# In this example the standard library function `file:consult` returns a
28+
# [tuple](http://erlang.org/doc/reference_manual/data_types.html#id69044)
29+
# with two elements: an [atom](http://erlang.org/doc/reference_manual/data_types.html#id64134)
30+
# (similar to a ruby symbol) and a variable containing ancillary data. On
31+
# success it returns the atom `ok` and the data from the file. On failure it
32+
# returns `error` and a string with an explanation of the problem. With this
33+
# pattern there is no ambiguity regarding success or failure. If the file is
34+
# empty the return value cannot be misinterpreted as an error. And when an
35+
# error occurs the return value provides useful information.
36+
#
37+
# In Ruby we tend to return `nil` when an error occurs or else we raise an
38+
# exception. Both of these idioms are problematic. Returning `nil` is
39+
# ambiguous because `nil` may also be a valid value. It also lacks
40+
# information pertaining to the nature of the error. Raising an exception
41+
# is both expensive and usurps the normal flow of control. All of these
42+
# problems can be solved with the use of a `Maybe`.
43+
#
44+
# A `Maybe` is unambiguous with regard to whether or not it contains a value.
45+
# When `Just` it contains a value, when `Nothing` it does not. When `Just`
46+
# the value it contains may be `nil`, which is perfectly valid. When
47+
# `Nothing` the reason for the lack of a value is contained as well. The
48+
# previous Erlang example can be duplicated in Ruby in a principled way by
49+
# having functions return `Maybe` objects:
50+
#
51+
# ```ruby
52+
# result = MyFileUtils.consult("data.dat") # returns a Maybe
53+
# if result.just?
54+
# do_something_useful(result.just) # or result.value
55+
# else
56+
# logger.error(result.nothing) # or result.reason
57+
# end
58+
# ```
59+
#
60+
# @example Returning a Maybe from a Function
61+
# module MyFileUtils
62+
# def self.consult(path)
63+
# file = File.open(path, 'r')
64+
# Concurrent::Maybe.just(file.read)
65+
# rescue => ex
66+
# return Concurrent::Maybe.nothing(ex)
67+
# ensure
68+
# file.close if file
69+
# end
70+
# end
71+
#
72+
# maybe = MyFileUtils.consult('bogus.file')
73+
# maybe.just? #=> false
74+
# maybe.nothing? #=> true
75+
# maybe.nothing #=> #<Errno::ENOENT: No such file or directory @ rb_sysopen - bogus.file>
76+
#
77+
# maybe = MyFileUtils.consult('README.md')
78+
# maybe.just? #=> true
79+
# maybe.nothing? #=> false
80+
# maybe.just #=> "# Concurrent Ruby\n[![Gem Version..."
81+
#
82+
# @example Using Maybe with a Block
83+
# result = Concurrent::Maybe.from do
84+
# Client.find(10) # Client is an ActiveRecord model
85+
# end
86+
#
87+
# # -- if the record was found
88+
# result.just? #=> true
89+
# result.just #=> #<Client id: 10, first_name: "Ryan">
90+
#
91+
# # -- if the record was not found
92+
# result.just? #=> false
93+
# result.nothing #=> ActiveRecord::RecordNotFound
94+
#
95+
# @example Using Maybe with the Null Object Pattern
96+
# # In a Rails controller...
97+
# result = ClientService.new(10).find # returns a Maybe
98+
# render json: result.or(NullClient.new)
99+
#
100+
# @see https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Maybe.html Haskell Data.Maybe
101+
# @see https://github.com/purescript/purescript-maybe/blob/master/docs/Data.Maybe.md PureScript Data.Maybe
102+
class Maybe
103+
include Comparable
104+
105+
# Indicates that the given attribute has not been set.
106+
# When `Just` the {#nothing} getter will return `NONE`.
107+
# When `Nothing` the {#just} getter will return `NONE`.
108+
NONE = Object.new.freeze
109+
110+
# The value of a `Maybe` when `Just`. Will be `NONE` when `Nothing`.
111+
attr_reader :just
112+
113+
# The reason for the `Maybe` when `Nothing`. Will be `NONE` when `Just`.
114+
attr_reader :nothing
115+
116+
private_class_method :new
117+
118+
# Create a new `Maybe` using the given block.
119+
#
120+
# Runs the given block passing all function arguments to the block as block
121+
# arguments. If the block runs to completion without raising an exception
122+
# a new `Just` is created with the value set to the return value of the
123+
# block. If the block raises an exception a new `Nothing` is created with
124+
# the reason being set to the raised exception.
125+
#
126+
# @param [Array<Object>] args Zero or more arguments to pass to the block.
127+
# @yield The block from which to create a new `Maybe`.
128+
# @yieldparam [Array<Object>] args Zero or more block arguments passed as
129+
# arguments to the function.
130+
#
131+
# @return [Maybe] The newly created object.
132+
#
133+
# @raise [ArgumentError] when no block given.
134+
def self.from(*args)
135+
raise ArgumentError.new('no block given') unless block_given?
136+
begin
137+
value = yield(*args)
138+
return new(value, NONE)
139+
rescue => ex
140+
return new(NONE, ex)
141+
end
142+
end
143+
144+
# Create a new `Just` with the given value.
145+
#
146+
# @param [Object] value The value to set for the new `Maybe` object.
147+
#
148+
# @return [Maybe] The newly created object.
149+
def self.just(value)
150+
return new(value, NONE)
151+
end
152+
153+
# Create a new `Nothing` with the given (optional) reason.
154+
#
155+
# @param [Exception] error The reason to set for the new `Maybe` object.
156+
# When given a string a new `StandardError` will be created with the
157+
# argument as the message. When no argument is given a new
158+
# `StandardError` with an empty message will be created.
159+
#
160+
# @return [Maybe] The newly created object.
161+
def self.nothing(error = '')
162+
if error.is_a?(Exception)
163+
nothing = error
164+
else
165+
nothing = StandardError.new(error.to_s)
166+
end
167+
return new(NONE, nothing)
168+
end
169+
170+
# Is this `Maybe` a `Just` (successfully fulfilled with a value)?
171+
#
172+
# @return [Boolean] True if `Just` or false if `Nothing`.
173+
def just?
174+
! nothing?
175+
end
176+
alias :fulfilled? :just?
177+
178+
# Is this `Maybe` a `nothing` (rejected with an exception upon fulfillment)?
179+
#
180+
# @return [Boolean] True if `Nothing` or false if `Just`.
181+
def nothing?
182+
@nothing != NONE
183+
end
184+
alias :rejected? :nothing?
185+
186+
alias :value :just
187+
188+
alias :reason :nothing
189+
190+
# Comparison operator.
191+
#
192+
# @return [Integer] 0 if self and other are both `Nothing`;
193+
# -1 if self is `Nothing` and other is `Just`;
194+
# 1 if self is `Just` and other is nothing;
195+
# `self.just <=> other.just` if both self and other are `Just`.
196+
def <=>(other)
197+
if nothing?
198+
other.nothing? ? 0 : -1
199+
else
200+
other.nothing? ? 1 : just <=> other.just
201+
end
202+
end
203+
204+
# Return either the value of self or the given default value.
205+
#
206+
# @return [Object] The value of self when `Just`; else the given default.
207+
def or(other)
208+
just? ? just : other
209+
end
210+
211+
private
212+
213+
# Create a new `Maybe` with the given attributes.
214+
#
215+
# @param [Object] just The value when `Just` else `NONE`.
216+
# @param [Exception, Object] nothing The exception when `Nothing` else `NONE`.
217+
#
218+
# @return [Maybe] The new `Maybe`.
219+
#
220+
# @!visibility private
221+
def initialize(just, nothing)
222+
@just = just
223+
@nothing = nothing
224+
end
225+
end
226+
end

lib/concurrent/mvar.rb

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,40 @@ module Concurrent
2424
# Note that unlike the original Haskell paper, our `#take` is blocking. This is how
2525
# Haskell and Scala do it today.
2626
#
27-
# @!macro copy_options
27+
# @!macro [attach] copy_options
28+
# ## Copy Options
29+
#
30+
# Object references in Ruby are mutable. This can lead to serious
31+
# problems when the {#value} of an object is a mutable reference. Which
32+
# is always the case unless the value is a `Fixnum`, `Symbol`, or similar
33+
# "primative" data type. Each instance can be configured with a few
34+
# options that can help protect the program from potentially dangerous
35+
# operations. Each of these options can be optionally set when the oject
36+
# instance is created:
37+
#
38+
# * `:dup_on_deref` When true the object will call the `#dup` method on
39+
# the `value` object every time the `#value` methid is called
40+
# (default: false)
41+
# * `:freeze_on_deref` When true the object will call the `#freeze`
42+
# method on the `value` object every time the `#value` method is called
43+
# (default: false)
44+
# * `:copy_on_deref` When given a `Proc` object the `Proc` will be run
45+
# every time the `#value` method is called. The `Proc` will be given
46+
# the current `value` as its only argument and the result returned by
47+
# the block will be the return value of the `#value` call. When `nil`
48+
# this option will be ignored (default: nil)
49+
#
50+
# When multiple deref options are set the order of operations is strictly defined.
51+
# The order of deref operations is:
52+
# * `:copy_on_deref`
53+
# * `:dup_on_deref`
54+
# * `:freeze_on_deref`
55+
#
56+
# Because of this ordering there is no need to `#freeze` an object created by a
57+
# provided `:copy_on_deref` block. Simply set `:freeze_on_deref` to `true`.
58+
# Setting both `:dup_on_deref` to `true` and `:freeze_on_deref` to `true` is
59+
# as close to the behavior of a "pure" functional language (like Erlang, Clojure,
60+
# or Haskell) as we are likely to get in Ruby.
2861
#
2962
# ## See Also
3063
#
@@ -49,7 +82,14 @@ class MVar
4982
#
5083
# @param [Hash] opts the options controlling how the future will be processed
5184
#
52-
# @!macro deref_options
85+
# @!macro [attach] deref_options
86+
# @option opts [Boolean] :dup_on_deref (false) Call `#dup` before
87+
# returning the data from {#value}
88+
# @option opts [Boolean] :freeze_on_deref (false) Call `#freeze` before
89+
# returning the data from {#value}
90+
# @option opts [Proc] :copy_on_deref (nil) When calling the {#value}
91+
# method, call the given proc passing the internal value as the sole
92+
# argument then return the new value returned from the proc.
5393
def initialize(value = EMPTY, opts = {})
5494
@value = value
5595
@mutex = Mutex.new

0 commit comments

Comments
 (0)