monad-oxide 0.14.0 → 0.16.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f4a3e02d264fe9a27b7e48c58865eacf77db7a083d75d4c6383aa76243d65a9
4
- data.tar.gz: f422c2b96a295fa4bcfee050d494c43f8ee721dac43d8e609321cfecf2f0020c
3
+ metadata.gz: 527a364e877c458b44d2f94cc3d59adace690bbee75e6da602daef77319369e2
4
+ data.tar.gz: dc568c5996d9dc36ae83cebf34cec5289fe6ea8e319c4ad1dde0aae1231e1e58
5
5
  SHA512:
6
- metadata.gz: 0c146c13f5d0a6867ec4cd0426c2a765c611c6c71b83bb12a5872ef2adc0fea4fb9845b46c9925d0a731722420d1c603afbbb0e72c3890405790c66c9474e249
7
- data.tar.gz: 0732a70394523cfde5269d72ec6581ff7695bce800aea1cdd86e1c78574b81d419aaa8a38fb2dfde7b5e988673493f03261294b48e18ca37e6c1f22bdbd1c67a
6
+ metadata.gz: 8bc31b055fcff603efff03d5057836be330c031210f77397c169a220777f3acb4ac583aab47b09d320c5bf52b209dac44b16c626d3f66d334a5c24f8e9dc2fa8
7
+ data.tar.gz: 6d1c32430d663eb3471ff25f0eb42c79249599e131ce866d422bad051c6bb4e6d74221ef59a55714a90216ed640c7d259e649635a22cda4ebf5e7513c89404fe
data/lib/monad-oxide.rb CHANGED
@@ -4,6 +4,9 @@ require_relative './ok'
4
4
  require_relative './either'
5
5
  require_relative './left'
6
6
  require_relative './right'
7
+ require_relative './option'
8
+ require_relative './some'
9
+ require_relative './none'
7
10
  require_relative './array'
8
11
  require_relative './version'
9
12
 
@@ -11,6 +14,7 @@ require_relative './version'
11
14
  # The top level module for the monad-oxide library. Of interest, @see `Result',
12
15
  # @see `Err', and @see `Ok'.
13
16
  module MonadOxide
17
+
14
18
  module_function
15
19
 
16
20
  ##
@@ -25,6 +29,10 @@ module MonadOxide
25
29
  MonadOxide::Left.new(data)
26
30
  end
27
31
 
32
+ def none()
33
+ MonadOxide::None.new()
34
+ end
35
+
28
36
  ##
29
37
  # Create an `Ok' as a conveniece method.
30
38
  # @param data [Object] The inner data for this `Ok'.
@@ -33,8 +41,16 @@ module MonadOxide
33
41
  MonadOxide::Ok.new(data)
34
42
  end
35
43
 
44
+ def option(data)
45
+ data.nil?() ? none() : some(data)
46
+ end
47
+
36
48
  def right(data)
37
49
  MonadOxide::Right.new(data)
38
50
  end
39
51
 
52
+ def some(data)
53
+ MonadOxide::Some.new(data)
54
+ end
55
+
40
56
  end
data/lib/none.rb ADDED
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './option'
4
+
5
+ module MonadOxide
6
+
7
+ ##
8
+ # `None' represents the absence of a value in an `Option'.
9
+ #
10
+ # Any methods in Option that would process a present value (Some) will fall
11
+ # through with None instances.
12
+ class None < Option
13
+
14
+ ##
15
+ # Create a None.
16
+ def initialize()
17
+ end
18
+
19
+ ##
20
+ # Falls through. @see Option#and_then for how this is handled in either
21
+ # Option case, and @see Some#and_then for how this is handled in the Some
22
+ # case.
23
+ # @param f [Proc] Optional Proc - ignored.
24
+ # @yield An ignored block.
25
+ # @return [None] This None.
26
+ def and_then(f=nil, &block)
27
+ self
28
+ end
29
+
30
+ ##
31
+ # Falls through. @see Option#inspect_some for how this is handled in
32
+ # either Option case, and @see Some#inspect_some for how this is handled
33
+ # in the Some case.
34
+ # @param f [Proc] Optional Proc - ignored.
35
+ # @yield An ignored block.
36
+ # @return [None] This None.
37
+ def inspect_some(f=nil, &block)
38
+ self
39
+ end
40
+
41
+ ##
42
+ # Applies `f' or the block and returns the same `None'. No changes are
43
+ # applied. This is ideal for logging.
44
+ # @param f [Proc<B>] The function to call. Could be a block instead. Takes
45
+ # nothing, the return is ignored.
46
+ # @yield Will yield a block that takes nothing, the return is ignored.
47
+ # Same as `f' parameter.
48
+ # @return [Option] returns self.
49
+ def inspect_none(f=nil, &block)
50
+ (f || block).call()
51
+ self
52
+ end
53
+
54
+ ##
55
+ # Falls through. @see Option#map for how this is handled in either
56
+ # Option case, and @see Some#map for how this is handled in the Some
57
+ # case.
58
+ # @param f [Proc] Optional Proc - ignored.
59
+ # @yield An ignored block.
60
+ # @return [None] This None.
61
+ def map(f=nil, &block)
62
+ self
63
+ end
64
+
65
+ ##
66
+ # Applies `f' or the block and returns a new `Option' with the returned
67
+ # value.
68
+ # @param f [Proc<B>] The function to call. Could be a block instead. Takes
69
+ # nothing and returns a B.
70
+ # @yield Will yield a block that takes nothing and returns a B. Same as
71
+ # `f' parameter.
72
+ # @return [Option<B>] A new `Option<B>' whose `B' is the return of `f' or
73
+ # the block.
74
+ def map_none(f=nil, &block)
75
+ Some.new((f || block).call())
76
+ end
77
+
78
+ ##
79
+ # Identifies that this is a `None'.
80
+ # For counterparts:
81
+ # @see MonadOxide::Some#some?
82
+ # @see MonadOxide::Some#none?
83
+ # @see MonadOxide::None#some?
84
+ # @return [Boolean] `true` because this is a `None'.
85
+ def none?()
86
+ true
87
+ end
88
+
89
+ ##
90
+ # Invokes `f' or the block and returns the Option returned from that. The
91
+ # return type is enforced.
92
+ # @param f [Proc<Option<B>>] The function to call. Could be a block
93
+ # instead. Takes nothing and must return a [Option<B>].
94
+ # @yield Will yield a block that takes nothing and returns a Option<B>.
95
+ # Same as `f' parameter.
96
+ # @return [Some<B> | None] A new Option from `f' or the block. If `f'
97
+ # returns a non-Option, this will raise
98
+ # `OptionReturnExpectedError'.
99
+ def or_else(f=nil, &block)
100
+ option = (f || block).call()
101
+ if !option.kind_of?(Option)
102
+ raise OptionReturnExpectedError.new(option)
103
+ else
104
+ option
105
+ end
106
+ end
107
+
108
+ ##
109
+ # Identifies that this is not a `Some'.
110
+ # For counterparts:
111
+ # @see MonadOxide::Some#some?
112
+ # @see MonadOxide::Some#none?
113
+ # @see MonadOxide::None#none?
114
+ # @return [Boolean] `false` because this is not a `Some'.
115
+ def some?()
116
+ false
117
+ end
118
+
119
+ ##
120
+ # Dangerously try to access the `Option' data. If this is a `None', an
121
+ # exception will be raised. It is recommended to use this for tests only.
122
+ # @return [A] The inner data of this `Option'.
123
+ def unwrap()
124
+ raise UnwrapError.new(
125
+ "#{self.class} could not be unwrapped as a Some.",
126
+ )
127
+ end
128
+
129
+ ##
130
+ # Dangerously access the `None' data. If this is a `Some', an exception
131
+ # will be raised. It is recommended to use this for tests only.
132
+ # @return [nil] Returns nil for `None'.
133
+ def unwrap_none()
134
+ nil
135
+ end
136
+
137
+ end
138
+
139
+ end
data/lib/option.rb ADDED
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+ ################################################################################
3
+ # An Option represents a value that may or may not be present. Some languages
4
+ # also calls this a Maybe. This is represented by two subclasses: Some and
5
+ # None. Some represents a present value, and None means no value is present.
6
+ # It is advantageous to use this over `nil` because, unlike `nil`, you can
7
+ # compose over `Option`.
8
+ ################################################################################
9
+
10
+ module MonadOxide
11
+
12
+ ##
13
+ # Thie Exception signals an area under construction, or somehow the consumer
14
+ # wound up with a base class instance and not one of its
15
+ # subclasses. Implementors of new methods on subclasses should
16
+ # create methods on the base class as well which immediately raise this
17
+ # `Exception'.
18
+ class OptionMethodNotImplementedError < MonadOxideError; end
19
+
20
+ ##
21
+ # This `Exception' is produced when a method that expects the function or
22
+ # block to provide a `Result' but was given something else. Generally this
23
+ # Exception is not raised, but instead converts the Result into a an Err.
24
+ # Example methods with this behavior are Result#and_then and Result#or_else.
25
+ class OptionReturnExpectedError < MonadOxideError
26
+ ##
27
+ # The transformation expected a `Result' but got something else.
28
+ # @param data [Object] Whatever we got that wasn't a `Result'.
29
+ def initialize(data)
30
+ super("A Result was expected but got #{data.inspect}.")
31
+ data = @data
32
+ end
33
+ attr_reader(:data)
34
+ end
35
+
36
+ ##
37
+ # An Option is a chainable series of sequential transformations. The Option
38
+ # structure contains a `Some<A> | None`. This is the central location
39
+ # for documentation between both `Some' and `None'. It is best to think of
40
+ # any given `Some' or `None' as an `Option' instead. All methods on `Some'
41
+ # are present on `None' and vice versa. This way we can interchange one for
42
+ # the other during virtually any call.
43
+ #
44
+ # This is a base-class only, and you should never see instances of these in
45
+ # the wild.
46
+ class Option
47
+
48
+ def initialize(data)
49
+ raise NoMethodError.new('Do not use Option directly. See Some and None.')
50
+ end
51
+
52
+ ##
53
+ # Determine if this is a MonadOxide::Some.
54
+ # @return [Boolean] `true` if this is a MonadOxide::Some, `false`
55
+ # otherwise.
56
+ def some?()
57
+ false
58
+ end
59
+
60
+ ##
61
+ # Determine if this is a MonadOxide::None.
62
+ # @return [Boolean] `true` if this is a MonadOxide::None, `false`
63
+ # otherwise.
64
+ def none?()
65
+ false
66
+ end
67
+
68
+ ##
69
+ # In the case of `Some', applies `f' or the block over the data and
70
+ # returns a new `Some' with the returned value. For `None', this method
71
+ # falls through.
72
+ # @param f [Proc<A, B>] The function to call. Could be a block instead.
73
+ # Takes an [A=Object] and returns a B.
74
+ # @yield Will yield a block that takes an A and returns a B. Same as `f'
75
+ # parameter.
76
+ # @return [Option<B>] A new `Option<B>' whose `B' is the return of `f' or
77
+ # the block for `Some'. For `None', returns self.
78
+ def map(f=nil, &block)
79
+ raise OptionMethodNotImplementedError.new()
80
+ end
81
+
82
+ ##
83
+ # In the case of `None', applies `f' or the block and returns a new
84
+ # `Option' with the returned value. For `Some', this method falls through.
85
+ # @param f [Proc<B>] The function to call. Could be a block instead. Takes
86
+ # nothing and returns a B.
87
+ # @yield Will yield a block that takes nothing and returns a B. Same as
88
+ # `f' parameter.
89
+ # @return [Option<B>] A new `Option<B>' whose `B' is the return of `f' or
90
+ # the block for `None'. For `Some', returns self.
91
+ def map_none(f=nil, &block)
92
+ raise OptionMethodNotImplementedError.new()
93
+ end
94
+
95
+ ##
96
+ # In the case of `Some', applies `f' or the block over the data and
97
+ # returns the same `Some'. No changes are applied. This is ideal for
98
+ # logging. For `None', this method falls through.
99
+ # @param f [Proc<A, B>] The function to call. Could be a block instead.
100
+ # Takes an [A] the return is ignored.
101
+ # @yield Will yield a block that takes an A the return is ignored. Same as
102
+ # `f' parameter.
103
+ # @return [Option<A>] returns self.
104
+ def inspect_some(f=nil, &block)
105
+ raise OptionMethodNotImplementedError.new()
106
+ end
107
+
108
+ ##
109
+ # In the case of `None', applies `f' or the block and returns the same
110
+ # `None'. No changes are applied. This is ideal for logging. For `Some',
111
+ # this method falls through.
112
+ # @param f [Proc<B>] The function to call. Could be a block instead. Takes
113
+ # nothing, the return is ignored.
114
+ # @yield Will yield a block that takes nothing, the return is ignored.
115
+ # Same as `f' parameter.
116
+ # @return [Option] returns self.
117
+ def inspect_none(f=nil, &block)
118
+ raise OptionMethodNotImplementedError.new()
119
+ end
120
+
121
+ ##
122
+ # For `Some', invokes `f' or the block with the data and returns the
123
+ # Option returned from that.
124
+ #
125
+ # For `None', returns itself and the function/block are ignored.
126
+ #
127
+ # This method is used for control flow based on `Option' values.
128
+ #
129
+ # `and_then' is desirable for chaining together other Option based
130
+ # operations, or doing transformations where flipping from a `Some' to a
131
+ # `None' is desired. In cases where there is little/no risk of a `None'
132
+ # state, @see Option#map.
133
+ #
134
+ # The `None' equivalent operation is @see Option#or_else.
135
+ #
136
+ # The return type is enforced.
137
+ #
138
+ # @param f [Proc<A, Option<B>>] The function to call. Could be a block
139
+ # instead. Takes an [A=Object] and must return a [Option<B>].
140
+ # @yield Will yield a block that takes an A and returns a Option<B>. Same
141
+ # as `f' parameter.
142
+ # @return [Some<B> | None] A new Option from `f' or the block. If `f'
143
+ # returns a non-Option, this will raise
144
+ # `OptionReturnExpectedError'.
145
+ def and_then(f=nil, &block)
146
+ raise OptionMethodNotImplementedError.new()
147
+ end
148
+
149
+ ##
150
+ # For `None', invokes `f' or the block and returns the Option returned
151
+ # from that.
152
+ #
153
+ # For `Some', returns itself and the function/block are ignored.
154
+ #
155
+ # This method is used for control flow based on `Option' values.
156
+ #
157
+ # `or_else' is desirable for chaining together other Option based
158
+ # operations, or doing transformations where flipping from a `None' to a
159
+ # `Some' is desired.
160
+ #
161
+ # The `Some' equivalent operation is @see Option#and_then.
162
+ #
163
+ # The return type is enforced.
164
+ #
165
+ # @param f [Proc<Option<B>>] The function to call. Could be a block
166
+ # instead. Takes nothing and must return a [Option<B>].
167
+ # @yield Will yield a block that takes nothing and returns a Option<B>.
168
+ # Same as `f' parameter.
169
+ # @return [Some<B> | None] A new Option from `f' or the block. If `f'
170
+ # returns a non-Option, this will raise
171
+ # `OptionReturnExpectedError'.
172
+ def or_else(f=nil, &block)
173
+ raise OptionMethodNotImplementedError.new()
174
+ end
175
+
176
+ ##
177
+ # Dangerously access the `Option' data. If this is a `None', an exception
178
+ # will be raised. It is recommended to use this for tests only.
179
+ # @raise [UnwrapError] if called on a `None'.
180
+ # @return [A] - The inner data of this `Some'.
181
+ def unwrap()
182
+ raise OptionMethodNotImplementedError.new()
183
+ end
184
+
185
+ ##
186
+ # Dangerously access the `Option' data. If this is a `Some', an exception
187
+ # will be raised. It is recommended to use this for tests only.
188
+ # @raise [UnwrapError] if called on a `Some'.
189
+ # @return [nil] - Returns nil for `None'.
190
+ def unwrap_none()
191
+ raise OptionMethodNotImplementedError.new()
192
+ end
193
+
194
+ ##
195
+ # Use pattern matching to work with both Some and None variants. This is
196
+ # useful when it is desirable to have both variants handled in the same
197
+ # location. It can also be useful when either variant can coerced into a
198
+ # non-Option type.
199
+ #
200
+ # Ruby has no built-in pattern matching, but the next best thing is a
201
+ # Hash using the Option classes themselves as the keys.
202
+ #
203
+ # Tests for this are found in Some and None's tests.
204
+ #
205
+ # @param matcher [Hash<Class, Proc<T, R>] matcher The matcher to match
206
+ # upon.
207
+ # @option matcher [Proc] MonadOxide::Some The branch to execute for Some.
208
+ # @option matcher [Proc] MonadOxide::None The branch to execute for None.
209
+ # @return [R] The return value of the executed Proc.
210
+ def match(matcher)
211
+ if self.kind_of?(None)
212
+ matcher[self.class].call()
213
+ else
214
+ matcher[self.class].call(@data)
215
+ end
216
+ end
217
+
218
+ end
219
+
220
+ end
data/lib/some.rb ADDED
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './option'
4
+ require_relative './error'
5
+
6
+ module MonadOxide
7
+
8
+ ##
9
+ # `Some' represents a present value in an `Option'. For most operations,
10
+ # `Some' will perform some operation.
11
+ class Some < Option
12
+
13
+ ##
14
+ # Constructs a `Some' with the data provided.
15
+ # @param data [Object] The inner data this Option encapsulates.
16
+ def initialize(data)
17
+ @data = data
18
+ end
19
+
20
+ ##
21
+ # Invokes `f' or the block with the data and returns the Option returned
22
+ # from that. The return type is enforced.
23
+ # @param f [Proc<A, Option<B>>] The function to call. Could be a block
24
+ # instead. Takes an [A=Object] and must return a [Option<B>].
25
+ # @yield Will yield a block that takes an A and returns a Option<B>. Same
26
+ # as `f' parameter.
27
+ # @return [Some<B> | None] A new Option from `f' or the block. If `f'
28
+ # returns a non-Option, this will raise
29
+ # `OptionReturnExpectedError'.
30
+ def and_then(f=nil, &block)
31
+ option = (f || block).call(@data)
32
+ if !option.kind_of?(Option)
33
+ raise OptionReturnExpectedError.new(option)
34
+ else
35
+ option
36
+ end
37
+ end
38
+
39
+ ##
40
+ # Applies `f' or the block over the data and returns the same `Some'. No
41
+ # changes are applied. This is ideal for logging.
42
+ # @param f [Proc<A, B>] The function to call. Could be a block instead.
43
+ # Takes an [A] the return is ignored.
44
+ # @yield Will yield a block that takes an A the return is ignored. Same as
45
+ # `f' parameter.
46
+ # @return [Option<A>] returns self.
47
+ def inspect_some(f=nil, &block)
48
+ (f || block).call(@data)
49
+ self
50
+ end
51
+
52
+ ##
53
+ # Falls through. @see Option#inspect_none for how this is handled in
54
+ # either Option case, and @see None#inspect_none for how this is handled
55
+ # in the None case.
56
+ # @param f [Proc] Optional Proc - ignored.
57
+ # @yield An ignored block.
58
+ # @return [Some] This Some.
59
+ def inspect_none(f=nil, &block)
60
+ self
61
+ end
62
+
63
+ ##
64
+ # Applies `f' or the block over the data and returns a new `Some' with
65
+ # the returned value.
66
+ # @param f [Proc<A, B>] The function to call. Could be a block instead.
67
+ # Takes an [A=Object] and returns a B.
68
+ # @yield Will yield a block that takes an A and returns a B. Same as `f'
69
+ # parameter.
70
+ # @return [Option<B>] A new `Some<B>' whose `B' is the return of `f' or
71
+ # the block.
72
+ def map(f=nil, &block)
73
+ self.class.new((f || block).call(@data))
74
+ end
75
+
76
+ ##
77
+ # This is a no-op for Some. @see None#map_none.
78
+ # @param f [Proc<A, B>] A dummy function. Not used.
79
+ # @yield A dummy block. Not used.
80
+ # @return [Option<A>] This `Some'.
81
+ def map_none(f=nil, &block)
82
+ self
83
+ end
84
+
85
+ ##
86
+ # Identifies that this is not a `None'.
87
+ # For counterparts:
88
+ # @see MonadOxide::Some#some?
89
+ # @see MonadOxide::None#some?
90
+ # @see MonadOxide::None#none?
91
+ # @return [Boolean] `false` because this is not a `None'.
92
+ def none?()
93
+ false
94
+ end
95
+
96
+ ##
97
+ # The None equivalent to Some#and_then. This is a no-op for Some. @see
98
+ # None#or_else.
99
+ # @param f [Proc<A, B>] A dummy function. Not used.
100
+ # @yield A dummy block. Not used.
101
+ # @return [Option<A>] This `Some'.
102
+ def or_else(f=nil, &block)
103
+ self
104
+ end
105
+
106
+ ##
107
+ # Identifies that this is a `Some'.
108
+ # For counterparts:
109
+ # @see MonadOxide::Some#none?
110
+ # @see MonadOxide::None#some?
111
+ # @see MonadOxide::None#none?
112
+ # @return [Boolean] `true` because this is a `Some'.
113
+ def some?()
114
+ true
115
+ end
116
+
117
+ ##
118
+ # Dangerously access the `Some' data. If this is a `None', an exception
119
+ # will be raised. It is recommended to use this for tests only.
120
+ # @return [A] The inner data of this `Some'.
121
+ def unwrap()
122
+ @data
123
+ end
124
+
125
+ ##
126
+ # Dangerously access the `None' data. If this is a `Some', an exception
127
+ # will be raised. It is recommended to use this for tests only.
128
+ # @return [nil] Returns nil for `None'.
129
+ def unwrap_none()
130
+ raise UnwrapError.new(
131
+ <<~EOE
132
+ #{self.class} with #{@data.inspect()} could not be unwrapped as a
133
+ None.
134
+ EOE
135
+ )
136
+ end
137
+
138
+ end
139
+
140
+ end
data/lib/version.rb CHANGED
@@ -3,5 +3,5 @@ module MonadOxide
3
3
  # This version is locked to 0.x.0 because semver is a lie and this project
4
4
  # uses CICD to push new versions. Any commits that appear on main will result
5
5
  # in a new version of the gem created and published.
6
- VERSION='0.14.0'
6
+ VERSION='0.16.0'
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monad-oxide
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Logan Barnett
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-06 00:00:00.000000000 Z
11
+ date: 2025-11-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: org-ruby
@@ -81,9 +81,12 @@ files:
81
81
  - lib/error.rb
82
82
  - lib/left.rb
83
83
  - lib/monad-oxide.rb
84
+ - lib/none.rb
84
85
  - lib/ok.rb
86
+ - lib/option.rb
85
87
  - lib/result.rb
86
88
  - lib/right.rb
89
+ - lib/some.rb
87
90
  - lib/version.rb
88
91
  homepage:
89
92
  licenses: