dry-monads 0.2.1 → 0.3.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
  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.