dry-monads 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|