dry-monads 0.3.1 → 0.4.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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +7 -15
- data/.yardopts +4 -0
- data/CHANGELOG.md +32 -3
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +7 -1
- data/dry-monads.gemspec +3 -2
- data/lib/dry/monads.rb +50 -27
- data/lib/dry/monads/either.rb +1 -193
- data/lib/dry/monads/errors.rb +15 -0
- data/lib/dry/monads/list.rb +8 -7
- data/lib/dry/monads/maybe.rb +44 -25
- data/lib/dry/monads/result.rb +250 -0
- data/lib/dry/monads/result/fixed.rb +34 -0
- data/lib/dry/monads/right_biased.rb +79 -6
- data/lib/dry/monads/try.rb +86 -42
- data/lib/dry/monads/version.rb +1 -1
- data/lib/json/add/dry/monads/maybe.rb +1 -1
- metadata +32 -6
@@ -0,0 +1,15 @@
|
|
1
|
+
module Dry
|
2
|
+
module Monads
|
3
|
+
class UnwrapError < StandardError
|
4
|
+
def initialize(ctx)
|
5
|
+
super("value! was called on #{ ctx.inspect }")
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class InvalidFailureTypeError < StandardError
|
10
|
+
def initialize(failure)
|
11
|
+
super("Cannot create Failure from #{ failure.inspect }, it doesn't meet constraints")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/dry/monads/list.rb
CHANGED
@@ -25,7 +25,7 @@ module Dry
|
|
25
25
|
elsif value.respond_to?(:to_ary)
|
26
26
|
List.new(value.to_ary, type)
|
27
27
|
else
|
28
|
-
raise
|
28
|
+
raise TypeError, "Can't coerce #{value.inspect} to List"
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
@@ -89,14 +89,14 @@ module Dry
|
|
89
89
|
end
|
90
90
|
|
91
91
|
# Maps a block over the list. Acts as `Array#map`.
|
92
|
-
#
|
92
|
+
# Note that this method returns an Array instance, not a List
|
93
93
|
#
|
94
|
-
# @return [List]
|
94
|
+
# @return [List,Enumerator]
|
95
95
|
def map(&block)
|
96
96
|
if block
|
97
97
|
fmap(block)
|
98
98
|
else
|
99
|
-
|
99
|
+
value.map(&block)
|
100
100
|
end
|
101
101
|
end
|
102
102
|
|
@@ -118,7 +118,8 @@ module Dry
|
|
118
118
|
#
|
119
119
|
# @return [String]
|
120
120
|
def inspect
|
121
|
-
"
|
121
|
+
type_ann = typed? ? "<#{ type.name.split('::').last }>" : ''
|
122
|
+
"List#{ type_ann }#{ value.inspect }"
|
122
123
|
end
|
123
124
|
alias_method :to_s, :inspect
|
124
125
|
|
@@ -235,11 +236,11 @@ module Dry
|
|
235
236
|
|
236
237
|
# Traverses the list with a block (or without it).
|
237
238
|
# This methods "flips" List structure with the given monad (obtained from the type).
|
238
|
-
# Note that traversing requires the list to be
|
239
|
+
# Note that traversing requires the list to be typed.
|
239
240
|
# Also if a block given, its returning type must be equal list's type.
|
240
241
|
#
|
241
242
|
# @example
|
242
|
-
# List<
|
243
|
+
# List<Result>[Success(1), Success(2)].traverse # => Success([1, 2])
|
243
244
|
# List<Maybe>[Some(1), None, Some(3)].traverse # => None
|
244
245
|
#
|
245
246
|
# @return [Monad] Result is a monadic value
|
data/lib/dry/monads/maybe.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'dry/equalizer'
|
2
|
+
require 'dry/core/deprecations'
|
3
|
+
require 'dry/core/constants'
|
2
4
|
|
3
5
|
require 'dry/monads/right_biased'
|
4
6
|
require 'dry/monads/transformer'
|
@@ -9,9 +11,10 @@ module Dry
|
|
9
11
|
#
|
10
12
|
# @api public
|
11
13
|
class Maybe
|
12
|
-
include Dry::Equalizer(:value)
|
13
14
|
include Transformer
|
14
15
|
|
16
|
+
extend Dry::Core::Deprecations[:'dry-monads']
|
17
|
+
|
15
18
|
class << self
|
16
19
|
# Wraps the given value with into a Maybe object.
|
17
20
|
#
|
@@ -39,11 +42,13 @@ module Dry
|
|
39
42
|
def none?
|
40
43
|
is_a?(None)
|
41
44
|
end
|
45
|
+
alias_method :failure?, :none?
|
42
46
|
|
43
47
|
# Returns true for an instance of a {Maybe::Some} monad.
|
44
48
|
def some?
|
45
49
|
is_a?(Some)
|
46
50
|
end
|
51
|
+
alias_method :success?, :some?
|
47
52
|
|
48
53
|
# Returns self, added to keep the interface compatible with that of Either monad types.
|
49
54
|
#
|
@@ -64,10 +69,9 @@ module Dry
|
|
64
69
|
#
|
65
70
|
# @api public
|
66
71
|
class Some < Maybe
|
72
|
+
include Dry::Equalizer(:value!)
|
67
73
|
include RightBiased::Right
|
68
74
|
|
69
|
-
attr_reader :value
|
70
|
-
|
71
75
|
def initialize(value)
|
72
76
|
raise ArgumentError, 'nil cannot be some' if value.nil?
|
73
77
|
@value = value
|
@@ -89,9 +93,9 @@ module Dry
|
|
89
93
|
|
90
94
|
# @return [String]
|
91
95
|
def to_s
|
92
|
-
"Some(#{value.inspect})"
|
96
|
+
"Some(#{ @value.inspect })"
|
93
97
|
end
|
94
|
-
|
98
|
+
alias_method :inspect, :to_s
|
95
99
|
end
|
96
100
|
|
97
101
|
# Represents an absence of a value, i.e. the value nil.
|
@@ -100,14 +104,9 @@ module Dry
|
|
100
104
|
class None < Maybe
|
101
105
|
include RightBiased::Left
|
102
106
|
|
103
|
-
@instance = new
|
107
|
+
@instance = new.freeze
|
104
108
|
singleton_class.send(:attr_reader, :instance)
|
105
109
|
|
106
|
-
# @return [nil]
|
107
|
-
def value
|
108
|
-
nil
|
109
|
-
end
|
110
|
-
|
111
110
|
# If a block is given passes internal value to it and returns the result,
|
112
111
|
# otherwise simply returns the parameter val.
|
113
112
|
#
|
@@ -144,7 +143,18 @@ module Dry
|
|
144
143
|
def to_s
|
145
144
|
'None'
|
146
145
|
end
|
147
|
-
|
146
|
+
alias_method :inspect, :to_s
|
147
|
+
|
148
|
+
# @api private
|
149
|
+
def eql?(other)
|
150
|
+
other.is_a?(None)
|
151
|
+
end
|
152
|
+
alias_method :==, :eql?
|
153
|
+
|
154
|
+
# @api private
|
155
|
+
def hash
|
156
|
+
None.instance.object_id
|
157
|
+
end
|
148
158
|
end
|
149
159
|
|
150
160
|
# A module that can be included for easier access to Maybe monads.
|
@@ -153,22 +163,31 @@ module Dry
|
|
153
163
|
Some = Some
|
154
164
|
None = None
|
155
165
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
Maybe
|
160
|
-
|
166
|
+
module Constructors
|
167
|
+
# @param value [Object] the value to be stored in the monad
|
168
|
+
# @return [Maybe::Some, Maybe::None]
|
169
|
+
def Maybe(value)
|
170
|
+
Maybe.lift(value)
|
171
|
+
end
|
161
172
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
173
|
+
# @param value [Object] the value to be stored in the monad
|
174
|
+
# @return [Maybe::Some]
|
175
|
+
def Some(value = Dry::Core::Constants::Undefined, &block)
|
176
|
+
if value.equal?(Dry::Core::Constants::Undefined)
|
177
|
+
raise ArgumentError, 'No value given' if block.nil?
|
178
|
+
Some.new(block)
|
179
|
+
else
|
180
|
+
Some.new(value)
|
181
|
+
end
|
182
|
+
end
|
167
183
|
|
168
|
-
|
169
|
-
|
170
|
-
|
184
|
+
# @return [Maybe::None]
|
185
|
+
def None
|
186
|
+
None.instance
|
187
|
+
end
|
171
188
|
end
|
189
|
+
|
190
|
+
include Constructors
|
172
191
|
end
|
173
192
|
end
|
174
193
|
end
|
@@ -0,0 +1,250 @@
|
|
1
|
+
require 'dry/equalizer'
|
2
|
+
require 'dry/core/constants'
|
3
|
+
|
4
|
+
require 'dry/monads/right_biased'
|
5
|
+
require 'dry/monads/transformer'
|
6
|
+
require 'dry/monads/maybe'
|
7
|
+
|
8
|
+
module Dry
|
9
|
+
module Monads
|
10
|
+
# Represents an operation which either succeeded or failed.
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
class Result
|
14
|
+
include Transformer
|
15
|
+
|
16
|
+
attr_reader :success, :failure
|
17
|
+
|
18
|
+
class << self
|
19
|
+
# Wraps the given value with Success
|
20
|
+
#
|
21
|
+
# @param value [Object] the value to be stored inside Success
|
22
|
+
# @return [Result::Success]
|
23
|
+
def pure(value)
|
24
|
+
Success.new(value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns self, added to keep the interface compatible with other monads.
|
29
|
+
#
|
30
|
+
# @return [Result::Success, Result::Failure]
|
31
|
+
def to_result
|
32
|
+
self
|
33
|
+
end
|
34
|
+
alias_method :to_either, :to_result
|
35
|
+
|
36
|
+
# Returns the Result monad.
|
37
|
+
# This is how we're doing polymorphism in Ruby 😕
|
38
|
+
#
|
39
|
+
# @return [Monad]
|
40
|
+
def monad
|
41
|
+
Result
|
42
|
+
end
|
43
|
+
|
44
|
+
# Represents a value of a successful operation.
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
class Success < Result
|
48
|
+
include RightBiased::Right
|
49
|
+
include Dry::Equalizer(:value!)
|
50
|
+
|
51
|
+
alias_method :success, :value!
|
52
|
+
|
53
|
+
# @param value [Object] a value of a successful operation
|
54
|
+
def initialize(value)
|
55
|
+
@value = value
|
56
|
+
end
|
57
|
+
|
58
|
+
# Apply the second function to value.
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def result(_, f)
|
62
|
+
f.(@value)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns false
|
66
|
+
def failure?
|
67
|
+
false
|
68
|
+
end
|
69
|
+
alias_method :left?, :failure?
|
70
|
+
|
71
|
+
# Returns true
|
72
|
+
def success?
|
73
|
+
true
|
74
|
+
end
|
75
|
+
alias_method :right?, :success?
|
76
|
+
|
77
|
+
# Does the same thing as #bind except it also wraps the value
|
78
|
+
# in an instance of Result::Success monad. This allows for easier
|
79
|
+
# chaining of calls.
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# Dry::Monads.Success(4).fmap(&:succ).fmap(->(n) { n**2 }) # => Success(25)
|
83
|
+
#
|
84
|
+
# @param args [Array<Object>] arguments will be transparently passed through to #bind
|
85
|
+
# @return [Result::Success]
|
86
|
+
def fmap(*args, &block)
|
87
|
+
Success.new(bind(*args, &block))
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [String]
|
91
|
+
def to_s
|
92
|
+
"Success(#{ @value.inspect })"
|
93
|
+
end
|
94
|
+
alias_method :inspect, :to_s
|
95
|
+
|
96
|
+
# @return [Maybe::Some]
|
97
|
+
def to_maybe
|
98
|
+
Kernel.warn 'Success(nil) transformed to None' if @value.nil?
|
99
|
+
Dry::Monads::Maybe(@value)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Transform to a Failure instance
|
103
|
+
#
|
104
|
+
# @return [Result::Failure]
|
105
|
+
def flip
|
106
|
+
Failure.new(@value)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Represents a value of a failed operation.
|
111
|
+
#
|
112
|
+
# @api public
|
113
|
+
class Failure < Result
|
114
|
+
include RightBiased::Left
|
115
|
+
include Dry::Equalizer(:failure)
|
116
|
+
|
117
|
+
# @api private
|
118
|
+
def failure
|
119
|
+
@value
|
120
|
+
end
|
121
|
+
alias_method :left, :failure
|
122
|
+
|
123
|
+
# @param value [Object] a value in an error state
|
124
|
+
def initialize(value)
|
125
|
+
@value = value
|
126
|
+
end
|
127
|
+
|
128
|
+
# Apply the first function to value.
|
129
|
+
#
|
130
|
+
# @api public
|
131
|
+
def result(f, _)
|
132
|
+
f.(@value)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns true
|
136
|
+
def failure?
|
137
|
+
true
|
138
|
+
end
|
139
|
+
alias_method :left?, :failure?
|
140
|
+
|
141
|
+
# Returns false
|
142
|
+
def success?
|
143
|
+
false
|
144
|
+
end
|
145
|
+
alias_method :right?, :success?
|
146
|
+
|
147
|
+
# If a block is given passes internal value to it and returns the result,
|
148
|
+
# otherwise simply returns the first argument.
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# Dry::Monads.Failure(ArgumentError.new('error message')).or(&:message) # => "error message"
|
152
|
+
#
|
153
|
+
# @param args [Array<Object>] arguments that will be passed to a block
|
154
|
+
# if one was given, otherwise the first
|
155
|
+
# value will be returned
|
156
|
+
# @return [Object]
|
157
|
+
def or(*args)
|
158
|
+
if block_given?
|
159
|
+
yield(@value, *args)
|
160
|
+
else
|
161
|
+
args[0]
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# A lifted version of `#or`. Wraps the passed value or the block result with Result::Success.
|
166
|
+
#
|
167
|
+
# @example
|
168
|
+
# Dry::Monads.Failure.new('no value').or_fmap('value') # => Success("value")
|
169
|
+
# Dry::Monads.Failure.new('no value').or_fmap { 'value' } # => Success("value")
|
170
|
+
#
|
171
|
+
# @param args [Array<Object>] arguments will be passed to the underlying `#or` call
|
172
|
+
# @return [Result::Success] Wrapped value
|
173
|
+
def or_fmap(*args, &block)
|
174
|
+
Success.new(self.or(*args, &block))
|
175
|
+
end
|
176
|
+
|
177
|
+
# @return [String]
|
178
|
+
def to_s
|
179
|
+
"Failure(#{ @value.inspect })"
|
180
|
+
end
|
181
|
+
alias_method :inspect, :to_s
|
182
|
+
|
183
|
+
# @return [Maybe::None]
|
184
|
+
def to_maybe
|
185
|
+
Maybe::None.instance
|
186
|
+
end
|
187
|
+
|
188
|
+
# Transform to a Success instance
|
189
|
+
#
|
190
|
+
# @return [Result::Success]
|
191
|
+
def flip
|
192
|
+
Success.new(@value)
|
193
|
+
end
|
194
|
+
|
195
|
+
# @see Dry::Monads::RightBiased::Left#value_or
|
196
|
+
def value_or(val = nil)
|
197
|
+
if block_given?
|
198
|
+
yield(@value)
|
199
|
+
else
|
200
|
+
val
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# @param other [Result]
|
205
|
+
# @return [Boolean]
|
206
|
+
def ===(other)
|
207
|
+
Failure === other && failure === other.failure
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# A module that can be included for easier access to Result monads.
|
212
|
+
module Mixin
|
213
|
+
Success = Dry::Monads::Result::Success
|
214
|
+
Failure = Dry::Monads::Result::Failure
|
215
|
+
|
216
|
+
module Constructors
|
217
|
+
# @param value [Object] the value to be stored in the monad
|
218
|
+
# @return [Result::Success]
|
219
|
+
def Success(value = Dry::Core::Constants::Undefined, &block)
|
220
|
+
if value.equal?(Dry::Core::Constants::Undefined)
|
221
|
+
raise ArgumentError, 'No value given' if block.nil?
|
222
|
+
Success.new(block)
|
223
|
+
else
|
224
|
+
Success.new(value)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
alias_method :Right, :Success
|
228
|
+
|
229
|
+
# @param value [Object] the value to be stored in the monad
|
230
|
+
# @return [Result::Failure]
|
231
|
+
def Failure(value = Dry::Core::Constants::Undefined, &block)
|
232
|
+
if value.equal?(Dry::Core::Constants::Undefined)
|
233
|
+
raise ArgumentError, 'No value given' if block.nil?
|
234
|
+
Failure.new(block)
|
235
|
+
else
|
236
|
+
Failure.new(value)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
alias_method :Left, :Failure
|
240
|
+
end
|
241
|
+
|
242
|
+
include Constructors
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
Either = Result
|
247
|
+
Result::Right = Result::Success
|
248
|
+
Result::Left = Result::Failure
|
249
|
+
end
|
250
|
+
end
|