dry-monads 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f0acc62dda69a191664c47cae89550fa7e248b34
4
- data.tar.gz: afd7e702a5dfb513b1f797890cdc465d5c1aaa57
3
+ metadata.gz: 2d763397f947c61f8b46fa0997e72b24b0c6b896
4
+ data.tar.gz: 99c80e46741e351e19f107dd71c01e8a7a66beaf
5
5
  SHA512:
6
- metadata.gz: cbfa1c6b7446d082f00539c7764becdccc2ccdcc6045d7ff2c4006aea2c98651ac189a4db88b3fb1585ded0b375597e08a9248827651d61350e581a6fd908b3b
7
- data.tar.gz: 2f6116133861c76fc8f7975c8e44badd1c901de101fe4f91b9c7fe28242a89534fd1b1ae904481cd441a02bf76be9064377b1fac48b84142e843bce4ff9e19d4
6
+ metadata.gz: 134d7bc216b65df0b1ed974fc68d04521ec1766c527848c4df473057ae5175e42447cee84073e69b44d4fe3a8a9399918ffb8c7119d6eb65abf0f7aa873d521c
7
+ data.tar.gz: b25676d21087677f2a2276b0e09abb3e672ce0d12f4c50aeee88c72afa8aff0d4e6df70ace29efc00b699094b94fe44f14753063d3f63bc2653bc8c50839d5d5
@@ -1,25 +1,23 @@
1
1
  language: ruby
2
2
  dist: trusty
3
- sudo: required
3
+ sudo: false
4
4
  cache: bundler
5
5
  bundler_args: --without benchmarks
6
6
  script:
7
7
  - bundle exec rake spec
8
- - bundle exec rubocop
9
8
  after_success:
10
- - '[ "$TRAVIS_RUBY_VERSION" = "2.3.1" ] && [ "$TRAVIS_BRANCH" = "master" ] && bundle exec codeclimate-test-reporter'
9
+ # Send coverage report from the job #1 == current MRI release
10
+ - '[ "${TRAVIS_JOB_NUMBER#*.}" = "1" ] && [ "$TRAVIS_BRANCH" = "master" ] && bundle exec codeclimate-test-reporter'
11
11
  rvm:
12
+ - 2.4.0
13
+ - 2.3.3
14
+ - 2.2.6
12
15
  - 2.1.10
13
- - 2.2.5
14
- - 2.3.1
15
- - jruby-9.1.5.0
16
+ - jruby-9.1.7.0
16
17
  - ruby-head
17
- - rbx-3
18
18
  env:
19
19
  global:
20
20
  - JRUBY_OPTS='--dev -J-Xmx1024M'
21
- before_install:
22
- - gem update bundler
23
21
  matrix:
24
22
  allow_failures:
25
23
  - rvm: ruby-head
@@ -27,7 +25,9 @@ matrix:
27
25
  - rvm: rbx-3
28
26
  include:
29
27
  - rvm: jruby-head
30
- before_install: gem install bundler --no-ri --no-rdoc
28
+ before_install: gem update bundler
29
+ - rvm: rbx-3
30
+ before_install: gem update bundler
31
31
 
32
32
  notifications:
33
33
  email:
@@ -0,0 +1,67 @@
1
+ # v0.3.0 2017-03-16
2
+
3
+ ## Added
4
+ * Added `Either#either` that accepts two callbacks, runs the first if it is `Right` and the second otherwise (nkondratyev)
5
+ * Added `#fmap2` and `#fmap3` for mapping over nested structures like `List Either` and `Either Some` (flash-gordon)
6
+ * Added `Try#value_or` (dsounded)
7
+ * Added the `List` monad which acts as an immutable `Array` and plays nice with other monads. A common example is a list of `Either`s (flash-gordon)
8
+ * `#bind` made to work with keyword arguments as extra parameters to the block (flash-gordon)
9
+ * Added `List#traverse` that "flips" the list with an embedded monad (flash-gordon + damncabbage)
10
+ * Added `#tee` for all right-biased monads (flash-gordon)
11
+
12
+ [Compare v0.3.0...v0.2.1](https://github.com/dry-rb/dry-monads/compare/v0.2.1...v0.3.0)
13
+
14
+ # v0.2.1 2016-11-13
15
+
16
+ ## Added
17
+
18
+ * Added `Either#tee` that is similar to `Object#tap` but executes the block only for `Right` instances (saverio-kantox)
19
+
20
+ ## Fixed
21
+
22
+ * `Right(nil).to_maybe` now returns `None` with a warning instead of failing (orisaka)
23
+ * `Some#value_or` doesn't require an argument because `None#value_or` doesn't require it either if a block was passed (flash-gordon)
24
+
25
+ [Compare v0.2.1...v0.2.0](https://github.com/dry-rb/dry-monads/compare/v0.2.0...v0.2.1)
26
+
27
+ # v0.2.0 2016-09-18
28
+
29
+ ## Added
30
+
31
+ * Added `Maybe#to_json` as an opt-in extension for serialization to JSON (rocknruby)
32
+ * Added `Maybe#value_or` which returns you the underlying value with a fallback in a single method call (dsounded)
33
+
34
+ [Compare v0.1.1...v0.2.0](https://github.com/dry-rb/dry-monads/compare/v0.1.1...v0.2.0)
35
+
36
+ # v0.1.1 2016-08-25
37
+
38
+ ## Fixed
39
+
40
+ * Added explicit requires of `dry-equalizer`. This allows to safely load only specific monads (artofhuman)
41
+
42
+ [Compare v0.1.0...v0.1.1](https://github.com/dry-rb/dry-monads/compare/v0.1.0...v0.1.1)
43
+
44
+ # v0.1.0 2016-08-23
45
+
46
+ ## Added
47
+
48
+ * Support for passing extra arguments to the block in `.bind` and `.fmap` (flash-gordon)
49
+
50
+ ## Changed
51
+
52
+ * Dropped MRI 2.0 support (flash-gordon)
53
+
54
+ [Compare v0.0.2...v0.1.0](https://github.com/dry-rb/dry-monads/compare/v0.0.2...v0.1.0)
55
+
56
+ # v0.0.2 2016-06-29
57
+
58
+ ## Added
59
+
60
+ * Added `Either#to_either` so that you can rely on duck-typing when you work with different types of monads (timriley)
61
+ * Added `Maybe#to_maybe` for consistency with `#to_either` (flash-gordon)
62
+
63
+ [Compare v0.0.1...v0.0.2](https://github.com/dry-rb/dry-monads/compare/v0.0.1...v0.0.2)
64
+
65
+ # v0.0.1 2016-05-02
66
+
67
+ Initial release containing `Either`, `Maybe`, and `Try` monads.
data/Gemfile CHANGED
@@ -8,6 +8,6 @@ group :test do
8
8
  end
9
9
 
10
10
  group :tools do
11
- gem 'rubocop'
11
+ gem 'pry-byebug', platform: :mri
12
12
  gem 'pry'
13
13
  end
@@ -12,3 +12,5 @@ M = Dry::Monads
12
12
  require 'pry'
13
13
 
14
14
  binding.pry
15
+
16
+ p
@@ -1,6 +1,7 @@
1
1
  require 'dry/monads/either'
2
2
  require 'dry/monads/maybe'
3
3
  require 'dry/monads/try'
4
+ require 'dry/monads/list'
4
5
 
5
6
  module Dry
6
7
  # @api public
@@ -1,5 +1,8 @@
1
1
  require 'dry/equalizer'
2
2
 
3
+ require 'dry/monads/right_biased'
4
+ require 'dry/monads/transformer'
5
+
3
6
  module Dry
4
7
  module Monads
5
8
  # Represents a value which is either correct or an error.
@@ -7,19 +10,19 @@ module Dry
7
10
  # @api public
8
11
  class Either
9
12
  include Dry::Equalizer(:right, :left)
10
- attr_reader :right, :left
13
+ include Transformer
11
14
 
12
- # Returns true for an instance of a {Either::Right} monad.
13
- def right?
14
- is_a? Right
15
- end
16
- alias success? right?
15
+ attr_reader :right, :left
17
16
 
18
- # Returns true for an instance of a {Either::Left} monad.
19
- def left?
20
- is_a? Left
17
+ class << self
18
+ # Wraps the given value with Right
19
+ #
20
+ # @param value [Object] the value to be stored inside Right
21
+ # @return [Either::Right]
22
+ def pure(value)
23
+ Right.new(value)
24
+ end
21
25
  end
22
- alias failure? left?
23
26
 
24
27
  # Returns self, added to keep the interface compatible with other monads.
25
28
  #
@@ -28,10 +31,20 @@ module Dry
28
31
  self
29
32
  end
30
33
 
34
+ # Returns the Either monad.
35
+ # This is how we're doing polymorphism in Ruby 😕
36
+ #
37
+ # @return [Monad]
38
+ def monad
39
+ Either
40
+ end
41
+
31
42
  # Represents a value that is in a correct state, i.e. everything went right.
32
43
  #
33
44
  # @api public
34
45
  class Right < Either
46
+ include RightBiased::Right
47
+
35
48
  alias value right
36
49
 
37
50
  # @param right [Object] a value in a correct state
@@ -39,27 +52,24 @@ module Dry
39
52
  @right = right
40
53
  end
41
54
 
42
- # Calls the passed in Proc object with value stored in self
43
- # and returns the result.
44
- #
45
- # If proc is nil, it expects a block to be given and will yield to it.
46
- #
47
- # @example
48
- # Dry::Monads.Right(4).bind(&:succ) # => 5
55
+ # Apply the second function to value.
49
56
  #
50
- # @param [Array<Object>] args arguments that will be passed to a block
51
- # if one was given, otherwise the first
52
- # value assumed to be a Proc (callable)
53
- # object and the rest of args will be passed
54
- # to this object along with the internal value
55
- # @return [Object] result of calling proc or block on the internal value
56
- def bind(*args)
57
- if block_given?
58
- yield(value, *args)
59
- else
60
- args[0].call(value, *args.drop(1))
61
- end
57
+ # @api public
58
+ def either(_, f)
59
+ f.call(value)
60
+ end
61
+
62
+ # Returns false
63
+ def left?
64
+ false
62
65
  end
66
+ alias failure? left?
67
+
68
+ # Returns true
69
+ def right?
70
+ true
71
+ end
72
+ alias success? right?
63
73
 
64
74
  # Does the same thing as #bind except it also wraps the value
65
75
  # in an instance of Either::Right monad. This allows for easier
@@ -68,33 +78,12 @@ module Dry
68
78
  # @example
69
79
  # Dry::Monads.Right(4).fmap(&:succ).fmap(->(n) { n**2 }) # => Right(25)
70
80
  #
71
- # @param [Array<Object>] args arguments will be transparently passed through to #bind
81
+ # @param args [Array<Object>] arguments will be transparently passed through to #bind
72
82
  # @return [Either::Right]
73
83
  def fmap(*args, &block)
74
84
  Right.new(bind(*args, &block))
75
85
  end
76
86
 
77
- # Does the same thing as #bind except it returns the original monad
78
- # when the result is a Right.
79
- #
80
- # @example
81
- # Dry::Monads.Right(4).tee { Right('ok') } # => Right(4)
82
- # Dry::Monads.Right(4).tee { Left('fail') } # => Left('fail')
83
- #
84
- # @param [Array<Object>] args arguments will be transparently passed through to #bind
85
- # @return [Either]
86
- def tee(*args, &block)
87
- bind(*args, &block).bind { self }
88
- end
89
-
90
- # Ignores arguments and returns self. It exists to keep the interface
91
- # identical to that of {Either::Left}.
92
- #
93
- # @return [Either::Right]
94
- def or(*)
95
- self
96
- end
97
-
98
87
  # @return [String]
99
88
  def to_s
100
89
  "Right(#{value.inspect})"
@@ -112,6 +101,8 @@ module Dry
112
101
  #
113
102
  # @api public
114
103
  class Left < Either
104
+ include RightBiased::Left
105
+
115
106
  alias value left
116
107
 
117
108
  # @param left [Object] a value in an error state
@@ -119,29 +110,24 @@ module Dry
119
110
  @left = left
120
111
  end
121
112
 
122
- # Ignores the input parameter and returns self. It exists to keep the interface
123
- # identical to that of {Either::Right}.
113
+ # Apply the first function to value.
124
114
  #
125
- # @return [Either::Left]
126
- def bind(*)
127
- self
115
+ # @api public
116
+ def either(f, _)
117
+ f.call(value)
128
118
  end
129
119
 
130
- # Ignores the input parameter and returns self. It exists to keep the interface
131
- # identical to that of {Either::Right}.
132
- #
133
- # @return [Either::Left]
134
- def fmap(*)
135
- self
120
+ # Returns true
121
+ def left?
122
+ true
136
123
  end
124
+ alias failure? left?
137
125
 
138
- # Ignores the input parameter and returns self. It exists to keep the interface
139
- # identical to that of {Either::Right}.
140
- #
141
- # @return [Either::Left]
142
- def tee(*)
143
- self
126
+ # Returns false
127
+ def right?
128
+ false
144
129
  end
130
+ alias success? right?
145
131
 
146
132
  # If a block is given passes internal value to it and returns the result,
147
133
  # otherwise simply returns the parameter val.
@@ -149,7 +135,7 @@ module Dry
149
135
  # @example
150
136
  # Dry::Monads.Left(ArgumentError.new('error message')).or(&:message) # => "error message"
151
137
  #
152
- # @param [Array<Object>] args arguments that will be passed to a block
138
+ # @param args [Array<Object>] arguments that will be passed to a block
153
139
  # if one was given, otherwise the first
154
140
  # value will be returned
155
141
  # @return [Object]
@@ -161,6 +147,18 @@ module Dry
161
147
  end
162
148
  end
163
149
 
150
+ # A lifted version of `#or`. Wraps the passed value or the block result with Either::Right.
151
+ #
152
+ # @example
153
+ # Dry::Monads.Left.new('no value').or_fmap('value') # => Right("value")
154
+ # Dry::Monads.Left.new('no value').or_fmap { 'value' } # => Right("value")
155
+ #
156
+ # @param args [Array<Object>] arguments will be passed to the underlying `#or` call
157
+ # @return [Either::Right] Wrapped value
158
+ def or_fmap(*args, &block)
159
+ Right.new(self.or(*args, &block))
160
+ end
161
+
164
162
  # @return [String]
165
163
  def to_s
166
164
  "Left(#{value.inspect})"
@@ -0,0 +1,285 @@
1
+ require 'dry/equalizer'
2
+ require 'dry/monads/maybe'
3
+ require 'dry/monads/transformer'
4
+
5
+ module Dry
6
+ module Monads
7
+ class List
8
+ class << self
9
+ # Builds a list.
10
+ #
11
+ # @param values [Array<Object>] List elements
12
+ # @return [List]
13
+ def [](*values)
14
+ new(values)
15
+ end
16
+
17
+ # Coerces a value to a list. `nil` will be coerced to an empty list.
18
+ #
19
+ # @param value [Object] Value
20
+ # @param type [Monad] Embedded monad type (used in case of list of monadic values)
21
+ # @return [List]
22
+ def coerce(value, type = nil)
23
+ if value.nil?
24
+ List.new([], type)
25
+ elsif value.respond_to?(:to_ary)
26
+ List.new(value.to_ary, type)
27
+ else
28
+ raise ArgumentError, "Can't coerce #{value.inspect} to List"
29
+ end
30
+ end
31
+
32
+ # Wraps a value with a list.
33
+ #
34
+ # @param value [Object] any object
35
+ # @return [List]
36
+ def pure(value, type = nil)
37
+ new([value], type)
38
+ end
39
+ end
40
+
41
+ include Dry::Equalizer(:value, :type)
42
+ include Transformer
43
+
44
+ # Internal array value
45
+ attr_reader :value, :type
46
+
47
+ # @api private
48
+ def initialize(value, type = nil)
49
+ @value = value
50
+ @type = type
51
+ end
52
+
53
+ # Lifts a block/proc and runs it against each member of the list.
54
+ # The block must return a value coercible to a list.
55
+ # As in other monads if no block given the first argument will
56
+ # be treated as callable and used instead.
57
+ #
58
+ # @example
59
+ # Dry::Monads::List[1, 2].bind { |x| [x + 1] } # => List[2, 3]
60
+ # Dry::Monads::List[1, 2].bind(-> x { [x, x + 1] }) # => List[1, 2, 2, 3]
61
+ #
62
+ # @param args [Array<Object>] arguments will be passed to the block or proc
63
+ # @return [List]
64
+ def bind(*args)
65
+ if block_given?
66
+ List.coerce(value.map { |v| yield(v, *args) }.reduce([], &:+))
67
+ else
68
+ obj, *rest = args
69
+ List.coerce(value.map { |v| obj.(v, *rest) }.reduce([], &:+))
70
+ end
71
+ end
72
+
73
+ # Maps a block over the list. Acts as `Array#map`.
74
+ # As in other monads if no block given the first argument will
75
+ # be treated as callable and used instead.
76
+ #
77
+ # @example
78
+ # Dry::Monads::List[1, 2].fmap { |x| x + 1 } # => List[2, 3]
79
+ #
80
+ # @param args [Array<Object>] arguments will be passed to the block or proc
81
+ # @return [List]
82
+ def fmap(*args)
83
+ if block_given?
84
+ List.new(value.map { |v| yield(v, *args) })
85
+ else
86
+ obj, *rest = args
87
+ List.new(value.map { |v| obj.(v, *rest) })
88
+ end
89
+ end
90
+
91
+ # Maps a block over the list. Acts as `Array#map`.
92
+ # Requires a block.
93
+ #
94
+ # @return [List]
95
+ def map(&block)
96
+ if block
97
+ fmap(block)
98
+ else
99
+ raise ArgumentError, "Missing block"
100
+ end
101
+ end
102
+
103
+ # Concatenates two lists.
104
+ #
105
+ # @example
106
+ # Dry::Monads::List[1, 2] + Dry::Monads::List[3, 4] # => List[1, 2, 3, 4]
107
+ #
108
+ # @param other [List] Other list
109
+ # @return [List]
110
+ def +(other)
111
+ List.new(to_ary + other.to_ary)
112
+ end
113
+
114
+ # Returns a string representation of the list.
115
+ #
116
+ # @example
117
+ # Dry::Monads::List[1, 2, 3].inspect # => "List[1, 2, 3]"
118
+ #
119
+ # @return [String]
120
+ def inspect
121
+ "List#{ value.inspect }"
122
+ end
123
+ alias_method :to_s, :inspect
124
+
125
+ # Coerces to an array
126
+ alias_method :to_ary, :value
127
+ alias_method :to_a, :to_ary
128
+
129
+ # Returns the first element.
130
+ #
131
+ # @return [Object]
132
+ def first
133
+ value.first
134
+ end
135
+
136
+ # Returns the last element.
137
+ #
138
+ # @return [Object]
139
+ def last
140
+ value.last
141
+ end
142
+
143
+ # Folds the list from the left.
144
+ #
145
+ # @param initial [Object] Initial value
146
+ # @return [Object]
147
+ def fold_left(initial)
148
+ value.reduce(initial) { |acc, v| yield(acc, v) }
149
+ end
150
+ alias_method :foldl, :fold_left
151
+ alias_method :reduce, :fold_left
152
+
153
+ # Folds the list from the right.
154
+ #
155
+ # @param initial [Object] Initial value
156
+ # @return [Object]
157
+ def fold_right(initial)
158
+ value.reverse.reduce(initial) { |a, b| yield(b, a) }
159
+ end
160
+ alias_method :foldr, :fold_right
161
+
162
+ # Whether the list is empty.
163
+ #
164
+ # @return [TrueClass, FalseClass]
165
+ def empty?
166
+ value.empty?
167
+ end
168
+
169
+ # Sorts the list.
170
+ #
171
+ # @return [List]
172
+ def sort
173
+ coerce(value.sort)
174
+ end
175
+
176
+ # Filters elements with a block
177
+ #
178
+ # @return [List]
179
+ def filter
180
+ coerce(value.select { |e| yield(e) })
181
+ end
182
+ alias_method :select, :filter
183
+
184
+ # List size.
185
+ #
186
+ # @return [Integer]
187
+ def size
188
+ value.size
189
+ end
190
+
191
+ # Reverses the list.
192
+ #
193
+ # @return [List]
194
+ def reverse
195
+ coerce(value.reverse)
196
+ end
197
+
198
+ # Returns the first element wrapped with a `Maybe`.
199
+ #
200
+ # @return [Maybe<Object>]
201
+ def head
202
+ Maybe.coerce(value.first)
203
+ end
204
+
205
+ # Returns list's tail.
206
+ #
207
+ # @return [List]
208
+ def tail
209
+ coerce(value.drop(1))
210
+ end
211
+
212
+ # Turns the list into a types one.
213
+ # Type is required for some operations like .traverse.
214
+ #
215
+ # @param type [Monad] Monad instance
216
+ # @return [List] Typed list
217
+ def typed(type = nil)
218
+ if type.nil?
219
+ if size.zero?
220
+ raise ArgumentError, "Cannot infer monad for an empty list"
221
+ else
222
+ self.class.new(value, value[0].monad)
223
+ end
224
+ else
225
+ self.class.new(value, type)
226
+ end
227
+ end
228
+
229
+ # Whether the list is types
230
+ #
231
+ # @return [Boolean]
232
+ def typed?
233
+ !type.nil?
234
+ end
235
+
236
+ # Traverses the list with a block (or without it).
237
+ # This methods "flips" List structure with the given monad (obtained from the type).
238
+ # Note that traversing requires the list to be types.
239
+ # Also if a block given, its returning type must be equal list's type.
240
+ #
241
+ # @example
242
+ # List<Either>[Right(1), Right(2)].traverse # => Right([1, 2])
243
+ # List<Maybe>[Some(1), None, Some(3)].traverse # => None
244
+ #
245
+ # @return [Monad] Result is a monadic value
246
+ def traverse
247
+ unless typed?
248
+ raise StandardError, "Cannot traverse an untyped list"
249
+ end
250
+
251
+ foldl(type.pure(EMPTY)) { |acc, el|
252
+ acc.bind { |unwrapped|
253
+ mapped = block_given? ? yield(el) : el
254
+ mapped.fmap { |i| unwrapped + List[i] }
255
+ }
256
+ }
257
+ end
258
+
259
+ # Returns the List monad.
260
+ #
261
+ # @return [Monad]
262
+ def monad
263
+ List
264
+ end
265
+
266
+ private
267
+
268
+ def coerce(other)
269
+ self.class.coerce(other)
270
+ end
271
+
272
+ # Empty list
273
+ EMPTY = List.new([].freeze).freeze
274
+
275
+ module Mixin
276
+ List = List
277
+ L = List
278
+
279
+ def List(value)
280
+ List.coerce(value)
281
+ end
282
+ end
283
+ end
284
+ end
285
+ end
@@ -1,5 +1,8 @@
1
1
  require 'dry/equalizer'
2
2
 
3
+ require 'dry/monads/right_biased'
4
+ require 'dry/monads/transformer'
5
+
3
6
  module Dry
4
7
  module Monads
5
8
  # Represents a value which can exist or not, i.e. it could be nil.
@@ -7,15 +10,27 @@ module Dry
7
10
  # @api public
8
11
  class Maybe
9
12
  include Dry::Equalizer(:value)
13
+ include Transformer
10
14
 
11
- # Lifts the given value into Maybe::None or Maybe::Some monad.
12
- #
13
- # @param value [Object] the value to be stored in the monad
14
- # @return [Maybe::Some, Maybe::None]
15
- def self.lift(value)
16
- if value.nil?
17
- None.instance
18
- else
15
+ class << self
16
+ # Wraps the given value with into a Maybe object.
17
+ #
18
+ # @param value [Object] the value to be stored in the monad
19
+ # @return [Maybe::Some, Maybe::None]
20
+ def coerce(value)
21
+ if value.nil?
22
+ None.instance
23
+ else
24
+ Some.new(value)
25
+ end
26
+ end
27
+ alias_method :lift, :coerce
28
+
29
+ # Wraps the given value with `Some`.
30
+ #
31
+ # @param value [Object] the value to be stored inside Some
32
+ # @return [Maybe::Some]
33
+ def pure(value)
19
34
  Some.new(value)
20
35
  end
21
36
  end
@@ -37,10 +52,20 @@ module Dry
37
52
  self
38
53
  end
39
54
 
55
+ # Returns the Maybe monad.
56
+ # This is how we're doing polymorphism in Ruby 😕
57
+ #
58
+ # @return [Monad]
59
+ def monad
60
+ Maybe
61
+ end
62
+
40
63
  # Represents a value that is present, i.e. not nil.
41
64
  #
42
65
  # @api public
43
66
  class Some < Maybe
67
+ include RightBiased::Right
68
+
44
69
  attr_reader :value
45
70
 
46
71
  def initialize(value)
@@ -48,28 +73,6 @@ module Dry
48
73
  @value = value
49
74
  end
50
75
 
51
- # Calls the passed in Proc object with value stored in self
52
- # and returns the result.
53
- #
54
- # If proc is nil, it expects a block to be given and will yield to it.
55
- #
56
- # @example
57
- # Dry::Monads.Some(4).bind(&:succ) # => 5
58
- #
59
- # @param [Array<Object>] args arguments that will be passed to a block
60
- # if one was given, otherwise the first
61
- # value assumed to be a Proc (callable)
62
- # object and the rest of args will be passed
63
- # to this object along with the internal value
64
- # @return [Object] result of calling proc or block on the internal value
65
- def bind(*args)
66
- if block_given?
67
- yield(value, *args)
68
- else
69
- args[0].call(value, *args.drop(1))
70
- end
71
- end
72
-
73
76
  # Does the same thing as #bind except it also wraps the value
74
77
  # in an instance of Maybe::Some monad. This allows for easier
75
78
  # chaining of calls.
@@ -77,28 +80,13 @@ module Dry
77
80
  # @example
78
81
  # Dry::Monads.Some(4).fmap(&:succ).fmap(->(n) { n**2 }) # => Some(25)
79
82
  #
80
- # @param [Array<Object>] args arguments will be transparently passed through to #bind
83
+ # @param args [Array<Object>] arguments will be transparently passed through to #bind
81
84
  # @return [Maybe::Some, Maybe::None] Lifted result, i.e. nil will be mapped to None,
82
85
  # other values will be wrapped with Some
83
86
  def fmap(*args, &block)
84
87
  self.class.lift(bind(*args, &block))
85
88
  end
86
89
 
87
- # Ignores arguments and returns self. It exists to keep the interface
88
- # identical to that of {Maybe::None}.
89
- #
90
- # @return [Maybe::Some]
91
- def or(*)
92
- self
93
- end
94
-
95
- # Returns value. It exists to keep the interface identical to that of {Maybe::None}.
96
- #
97
- # @return [Object]
98
- def value_or(_val = nil)
99
- value
100
- end
101
-
102
90
  # @return [String]
103
91
  def to_s
104
92
  "Some(#{value.inspect})"
@@ -110,6 +98,8 @@ module Dry
110
98
  #
111
99
  # @api public
112
100
  class None < Maybe
101
+ include RightBiased::Left
102
+
113
103
  @instance = new
114
104
  singleton_class.send(:attr_reader, :instance)
115
105
 
@@ -118,22 +108,6 @@ module Dry
118
108
  nil
119
109
  end
120
110
 
121
- # Ignores arguments and returns self. It exists to keep the interface
122
- # identical to that of {Maybe::Some}.
123
- #
124
- # @return [Maybe::None]
125
- def bind(*)
126
- self
127
- end
128
-
129
- # Ignores arguments and returns self. It exists to keep the interface
130
- # identical to that of {Maybe::Some}.
131
- #
132
- # @return [Maybe::None]
133
- def fmap(*)
134
- self
135
- end
136
-
137
111
  # If a block is given passes internal value to it and returns the result,
138
112
  # otherwise simply returns the parameter val.
139
113
  #
@@ -141,7 +115,8 @@ module Dry
141
115
  # Dry::Monads.None.or('no value') # => "no value"
142
116
  # Dry::Monads.None.or { Time.now } # => current time
143
117
  #
144
- # @param val [Object, nil]
118
+ # @param args [Array<Object>] if no block given the first argument will be returned
119
+ # otherwise arguments will be transparently passed to the block
145
120
  # @return [Object]
146
121
  def or(*args)
147
122
  if block_given?
@@ -151,15 +126,18 @@ module Dry
151
126
  end
152
127
  end
153
128
 
154
- # Returns the passed value
129
+ # A lifted version of `#or`. Applies `Maybe.lift` to the passed value or
130
+ # to the block result.
155
131
  #
156
- # @returns [Object]
157
- def value_or(val = nil)
158
- if block_given?
159
- yield
160
- else
161
- val
162
- end
132
+ # @example
133
+ # Dry::Monads.None.or_fmap('no value') # => Some("no value")
134
+ # Dry::Monads.None.or_fmap { Time.now } # => Some(current time)
135
+ #
136
+ # @param args [Array<Object>] arguments will be passed to the underlying `#or` call
137
+ # @return [Maybe::Some, Maybe::None] Lifted `#or` result, i.e. nil will be mapped to None,
138
+ # other values will be wrapped with Some
139
+ def or_fmap(*args, &block)
140
+ Maybe.lift(self.or(*args, &block))
163
141
  end
164
142
 
165
143
  # @return [String]
@@ -0,0 +1,148 @@
1
+ module Dry
2
+ module Monads
3
+ module RightBiased
4
+ module Right
5
+ attr_reader :value
6
+
7
+ # Calls the passed in Proc object with value stored in self
8
+ # and returns the result.
9
+ #
10
+ # If proc is nil, it expects a block to be given and will yield to it.
11
+ #
12
+ # @example
13
+ # Dry::Monads.Right(4).bind(&:succ) # => 5
14
+ #
15
+ # @param [Array<Object>] args arguments that will be passed to a block
16
+ # if one was given, otherwise the first
17
+ # value assumed to be a Proc (callable)
18
+ # object and the rest of args will be passed
19
+ # to this object along with the internal value
20
+ # @return [Object] result of calling proc or block on the internal value
21
+ def bind(*args, **kwargs)
22
+ vargs, vkwargs = destructure(value)
23
+ kw = kwargs.empty? && vkwargs.empty? ? [] : [kwargs.merge(vkwargs)]
24
+
25
+ if block_given?
26
+ yield(*vargs, *args, *kw)
27
+ else
28
+ obj, *rest = args
29
+ obj.call(*vargs, *rest, *kw)
30
+ end
31
+ end
32
+
33
+ # Does the same thing as #bind except it returns the original monad
34
+ # when the result is a Right.
35
+ #
36
+ # @example
37
+ # Dry::Monads.Right(4).tee { Right('ok') } # => Right(4)
38
+ # Dry::Monads.Right(4).tee { Left('fail') } # => Left('fail')
39
+ #
40
+ # @param [Array<Object>] args arguments will be transparently passed through to #bind
41
+ # @return [RightBiased::Right]
42
+ def tee(*args, &block)
43
+ bind(*args, &block).bind { self }
44
+ end
45
+
46
+ # Abstract method for lifting a block over the monad type
47
+ # Must be implemented for a right-biased monad
48
+ #
49
+ # @return [RightBiased::Right]
50
+ def fmap(*)
51
+ raise NotImplementedError
52
+ end
53
+
54
+ # Ignores arguments and returns self. It exists to keep the interface
55
+ # identical to that of {RightBiased::Left}.
56
+ #
57
+ # @return [RightBiased::Right]
58
+ def or(*)
59
+ self
60
+ end
61
+
62
+ # A lifted version of `#or`. For {RightBiased::Right} acts in the same way as `#or`,
63
+ # that is returns itselt.
64
+ #
65
+ # @return [RightBiased::Right]
66
+ def or_fmap(*)
67
+ self
68
+ end
69
+
70
+ # Returns value. It exists to keep the interface identical to that of RightBiased::Left
71
+ #
72
+ # @return [Object]
73
+ def value_or(_val = nil)
74
+ value
75
+ end
76
+
77
+ private
78
+
79
+ # @api private
80
+ def destructure(*args, **kwargs)
81
+ [args, kwargs]
82
+ end
83
+ end
84
+
85
+ module Left
86
+ attr_reader :value
87
+
88
+ # Ignores the input parameter and returns self. It exists to keep the interface
89
+ # identical to that of {RightBiased::Right}.
90
+ #
91
+ # @return [RightBiased::Left]
92
+ def bind(*)
93
+ self
94
+ end
95
+
96
+ # Ignores the input parameter and returns self. It exists to keep the interface
97
+ # identical to that of {RightBiased::Right}.
98
+ #
99
+ # @return [RightBiased::Left]
100
+ def tee(*)
101
+ self
102
+ end
103
+
104
+ # Ignores the input parameter and returns self. It exists to keep the interface
105
+ # identical to that of {RightBiased::Right}.
106
+ #
107
+ # @return [RightBiased::Left]
108
+ def fmap(*)
109
+ self
110
+ end
111
+
112
+ # Left-biased #bind version.
113
+ #
114
+ # @example
115
+ # Dry::Monads.Left(ArgumentError.new('error message')).or(&:message) # => "error message"
116
+ # Dry::Monads.None.or('no value') # => "no value"
117
+ # Dry::Monads.None.or { Time.now } # => current time
118
+ #
119
+ # @return [Object]
120
+ def or(*)
121
+ raise NotImplementedError
122
+ end
123
+
124
+ # A lifted version of `#or`. This is basically `#or` + `#fmap`.
125
+ #
126
+ # @example
127
+ # Dry::Monads.None.or('no value') # => Some("no value")
128
+ # Dry::Monads.None.or { Time.now } # => Some(current time)
129
+ #
130
+ # @return [RightBiased::Left, RightBiased::Right]
131
+ def or_fmap(*)
132
+ raise NotImplementedError
133
+ end
134
+
135
+ # Returns the passed value
136
+ #
137
+ # @returns [Object]
138
+ def value_or(val = nil)
139
+ if block_given?
140
+ yield
141
+ else
142
+ val
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,42 @@
1
+ module Dry
2
+ module Monads
3
+ module Transformer
4
+ # Lifts a block/proc over the 2-level nested structure.
5
+ # This is essentially fmap . fmap (. is the function composition
6
+ # operator from Haskell) or the functor instance for
7
+ # a two-level monadic structure like List Either.
8
+ #
9
+ # @example
10
+ # List[Right(1), Left(1)].fmap2 { |x| x + 1 } # => List[Right(2), Left(1)]
11
+ # Right(None).fmap2 { |x| x + 1 } # => Right(None)
12
+ #
13
+ # @param args [Array<Object>] arguments will be passed to the block or the proc
14
+ # @return [Object] some monadic value
15
+ def fmap2(*args)
16
+ if block_given?
17
+ fmap { |a| a.fmap { |b| yield(b, *args) } }
18
+ else
19
+ func, *rest = args
20
+ fmap { |a| a.fmap { |b| func.(b, *rest) } }
21
+ end
22
+ end
23
+
24
+ # Lifts a block/proc over the 3-level nested structure.
25
+ #
26
+ # @example
27
+ # List[Right(Some(1)), Left(Some(1))].fmap3 { |x| x + 1 } # => List[Right(Some(2)), Left(Some(1))]
28
+ # Right(None).fmap3 { |x| x + 1 } # => Right(None)
29
+ #
30
+ # @param args [Array<Object>] arguments will be passed to the block or the proc
31
+ # @return [Object] some monadic value
32
+ def fmap3(*args)
33
+ if block_given?
34
+ fmap { |a| a.fmap { |b| b.fmap { |c| yield(c, *args) } } }
35
+ else
36
+ func, *rest = args
37
+ fmap { |a| a.fmap { |b| b.fmap { |c| func.(c, *rest) } } }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,7 @@
1
1
  require 'dry/equalizer'
2
2
 
3
+ require 'dry/monads/right_biased'
4
+
3
5
  module Dry
4
6
  module Monads
5
7
  # Represents a value which can be either success or a failure (an exception).
@@ -7,7 +9,7 @@ module Dry
7
9
  #
8
10
  # @api public
9
11
  class Try
10
- attr_reader :exception, :value
12
+ attr_reader :exception
11
13
 
12
14
  # Calls the passed in proc object and if successful stores the result in a
13
15
  # {Try::Success} monad, but if one of the specified exceptions was raised it stores
@@ -37,6 +39,13 @@ module Dry
37
39
  # @api public
38
40
  class Success < Try
39
41
  include Dry::Equalizer(:value, :catchable)
42
+ include RightBiased::Right
43
+
44
+ # Using #or is not a good practice, you should process exceptions
45
+ # explicitly hence we don't offer an easy way to ignore them.
46
+ # Use Try#to_maybe if you're sure you need `#or` for `Try`.
47
+ undef :or
48
+
40
49
  attr_reader :catchable
41
50
 
42
51
  # @param exceptions [Array<Exception>] list of exceptions to be rescued
@@ -46,6 +55,9 @@ module Dry
46
55
  @value = value
47
56
  end
48
57
 
58
+ alias bind_call bind
59
+ private :bind_call
60
+
49
61
  # Calls the passed in Proc object with value stored in self
50
62
  # and returns the result.
51
63
  #
@@ -56,18 +68,14 @@ module Dry
56
68
  # success.bind(->(n) { n / 2 }) # => 5
57
69
  # success.bind { |n| n / 0 } # => Try::Failure(ZeroDivisionError: divided by 0)
58
70
  #
59
- # @param [Array<Object>] args arguments that will be passed to a block
71
+ # @param args [Array<Object>] arguments that will be passed to a block
60
72
  # if one was given, otherwise the first
61
73
  # value assumed to be a Proc (callable)
62
74
  # object and the rest of args will be passed
63
75
  # to this object along with the internal value
64
76
  # @return [Object, Try::Failure]
65
- def bind(*args)
66
- if block_given?
67
- yield(value, *args)
68
- else
69
- args[0].call(value, *args.drop(1))
70
- end
77
+ def bind(*)
78
+ super
71
79
  rescue *catchable => e
72
80
  Failure.new(e)
73
81
  end
@@ -81,15 +89,13 @@ module Dry
81
89
  # success.fmap(&:succ).fmap(&:succ).value # => 12
82
90
  # success.fmap(&:succ).fmap { |n| n / 0 }.fmap(&:succ).value # => nil
83
91
  #
84
- # @param [Array<Object>] args extra arguments for the block, arguments are being processes
92
+ # @param args [Array<Object>] extra arguments for the block, arguments are being processes
85
93
  # just as in #bind
86
94
  # @return [Try::Success, Try::Failure]
87
95
  def fmap(*args, &block)
88
- if block
89
- Try.lift(catchable, -> { yield(value, *args) })
90
- else
91
- Try.lift(catchable, -> { args[0].call(value, *args.drop(1)) })
92
- end
96
+ Success.new(catchable, bind_call(*args, &block))
97
+ rescue *catchable => e
98
+ Failure.new(e)
93
99
  end
94
100
 
95
101
  # @return [Maybe]
@@ -114,28 +120,14 @@ module Dry
114
120
  # @api public
115
121
  class Failure < Try
116
122
  include Dry::Equalizer(:exception)
123
+ include RightBiased::Left
124
+ undef :or
117
125
 
118
126
  # @param exception [Exception]
119
127
  def initialize(exception)
120
128
  @exception = exception
121
129
  end
122
130
 
123
- # Ignores arguments and returns self. It exists to keep the interface
124
- # identical to that of {Try::Success}.
125
- #
126
- # @return [Try::Failure]
127
- def bind(*)
128
- self
129
- end
130
-
131
- # Ignores arguments and returns self. It exists to keep the interface
132
- # identical to that of {Try::Success}.
133
- #
134
- # @return [Try::Failure]
135
- def fmap(*)
136
- self
137
- end
138
-
139
131
  # @return [Maybe::None]
140
132
  def to_maybe
141
133
  Dry::Monads::None()
@@ -171,12 +163,14 @@ module Dry
171
163
  module Mixin
172
164
  Try = Try
173
165
 
166
+ DEFAULT_EXCEPTIONS = [StandardError].freeze
167
+
174
168
  # A convenience wrapper for {Try.lift}.
175
169
  # If no exceptions are provided it falls back to StandardError.
176
170
  # In general, relying on this behaviour is not recommended as it can lead to unnoticed
177
171
  # bugs and it is always better to explicitly specify a list of exceptions if possible.
178
172
  def Try(*exceptions, &f)
179
- catchable = exceptions.any? ? exceptions.flatten : [StandardError]
173
+ catchable = exceptions.empty? ? DEFAULT_EXCEPTIONS : exceptions.flatten
180
174
  Try.lift(catchable, f)
181
175
  end
182
176
  end
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  module Monads
3
- VERSION = '0.2.1'.freeze
3
+ VERSION = '0.3.0'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-monads
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikita Shilnikov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-11-13 00:00:00.000000000 Z
11
+ date: 2017-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-equalizer
@@ -76,9 +76,8 @@ files:
76
76
  - ".codeclimate.yml"
77
77
  - ".gitignore"
78
78
  - ".rspec"
79
- - ".rubocop.yml"
80
- - ".rubocop_todo.yml"
81
79
  - ".travis.yml"
80
+ - CHANGELOG.md
82
81
  - Gemfile
83
82
  - LICENSE
84
83
  - README.md
@@ -89,7 +88,10 @@ files:
89
88
  - lib/dry-monads.rb
90
89
  - lib/dry/monads.rb
91
90
  - lib/dry/monads/either.rb
91
+ - lib/dry/monads/list.rb
92
92
  - lib/dry/monads/maybe.rb
93
+ - lib/dry/monads/right_biased.rb
94
+ - lib/dry/monads/transformer.rb
93
95
  - lib/dry/monads/try.rb
94
96
  - lib/dry/monads/version.rb
95
97
  - lib/json/add/dry/monads/maybe.rb
@@ -114,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
116
  version: '0'
115
117
  requirements: []
116
118
  rubyforge_project:
117
- rubygems_version: 2.5.1
119
+ rubygems_version: 2.6.10
118
120
  signing_key:
119
121
  specification_version: 4
120
122
  summary: Common monads for Ruby.
@@ -1,64 +0,0 @@
1
- inherit_from: .rubocop_todo.yml
2
-
3
- AllCops:
4
- Exclude:
5
- - 'spec/integration/either_spec.rb'
6
- - 'vendor/**/*'
7
-
8
- Style/Documentation:
9
- Enabled: false
10
-
11
- Style/StringLiterals:
12
- Enabled: false
13
-
14
- Style/MethodName:
15
- Enabled: false
16
-
17
- Style/LambdaCall:
18
- Enabled: false
19
-
20
- Style/StabbyLambdaParentheses:
21
- Enabled: false
22
-
23
- Style/NestedParenthesizedCalls:
24
- Enabled: false
25
-
26
- Style/MultilineBlockChain:
27
- Enabled: false
28
-
29
- Style/GuardClause:
30
- Enabled: false
31
-
32
- Style/SymbolProc:
33
- Exclude:
34
- # This rule is broken in specs on purpose to make examples clearer
35
- - 'spec/**/*'
36
-
37
- Style/SignalException:
38
- Exclude:
39
- - 'spec/**/*'
40
-
41
- Style/ModuleFunction:
42
- Enabled: false
43
-
44
- Style/RescueModifier:
45
- Enabled: false
46
-
47
- Metrics/LineLength:
48
- Max: 110
49
-
50
- Style/FileName:
51
- Exclude:
52
- - 'lib/dry-monads.rb'
53
-
54
- Lint/Debugger:
55
- Exclude:
56
- - 'bin/console'
57
-
58
- Lint/HandleExceptions:
59
- Exclude:
60
- - 'spec/spec_helper.rb'
61
-
62
- Security/JSONLoad:
63
- Exclude:
64
- - 'spec/**/*'
@@ -1,7 +0,0 @@
1
- # This configuration was generated by
2
- # `rubocop --auto-gen-config`
3
- # on 2016-04-29 01:33:48 +0100 using RuboCop version 0.39.0.
4
- # The point is for the user to remove these configuration records
5
- # one by one as the offenses are removed from the code base.
6
- # Note that changes in the inspected code, or installation of new
7
- # versions of RuboCop, may require this file to be generated again.